diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 40a08c857..9d83d954c 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -47,8 +47,8 @@ dependencies { implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' //Dependency Injection - implementation 'com.google.dagger:dagger:2.38' - kapt 'com.google.dagger:dagger-compiler:2.38' + implementation 'com.google.dagger:dagger:2.39.1' + kapt 'com.google.dagger:dagger-compiler:2.39.1' compileOnly 'javax.annotation:javax.annotation-api:1.3.2' compileOnly 'com.github.pengrad:jdk9-deps:1.0' //App Compatibility and Material Design @@ -82,8 +82,8 @@ dependencies { //Analytics implementation 'com.amplitude:android-sdk:2.30.0' // Image Management Library - implementation("io.coil-kt:coil:1.2.2") - implementation("io.coil-kt:coil-gif:1.2.2") + implementation("io.coil-kt:coil:1.4.0") + implementation("io.coil-kt:coil-gif:1.4.0") //Tests testImplementation 'io.kotest:kotest-runner-junit5:4.6.2' @@ -116,10 +116,12 @@ dependencies { implementation 'com.nex3z:flow-layout:1.2.2' implementation 'androidx.core:core-ktx:1.6.0' - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' + implementation "androidx.fragment:fragment-ktx:1.3.6" implementation "androidx.paging:paging-runtime-ktx:3.0.1" implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' @@ -153,8 +155,8 @@ android { buildConfigField "String", "TESTING_LEVEL", "\"production\"" resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW" - versionCode 3052 - versionName "3.4" + versionCode 3070 + versionName "3.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/Habitica/res/layout/quest_menu_view.xml b/Habitica/res/layout/quest_menu_view.xml index d519ec75e..a156a763f 100644 --- a/Habitica/res/layout/quest_menu_view.xml +++ b/Habitica/res/layout/quest_menu_view.xml @@ -36,9 +36,8 @@ android:textSize="12sp"/> + + + + \ No newline at end of file diff --git a/Habitica/res/values-en-rGB/strings.xml b/Habitica/res/values-en-rGB/strings.xml index ef916d1ee..daad320c5 100644 --- a/Habitica/res/values-en-rGB/strings.xml +++ b/Habitica/res/values-en-rGB/strings.xml @@ -1098,5 +1098,5 @@ The Gem Sale is back to haunt the very end of this year’s Fall Gala! This is one last chance to get more Gems than ever, so stock up while it lasts! Between October 29th and November 2nd, simply purchase any Gem bundle like usual and your account will be credited with the promotional amount of Gems. More Gems to spend, share, or save for any future releases! View Gem Bundles - The Fall Gala is in full swing so we thought it was the perfect time to introduce our first ever Gem Sale! Now you will get more Gems with each purchase than ever before. - \ No newline at end of file + The Fall Gala is in full swing so we thought it was the perfect time for a Gem Sale! Now you will get more Gems with each purchase than ever before. + diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml index 64a224eea..784016d1d 100644 --- a/Habitica/res/values/strings.xml +++ b/Habitica/res/values/strings.xml @@ -1114,7 +1114,7 @@ How it works Limitations Between %s and %s, simply purchase any Gem bundle like usual and your account will be credited with the promotional amount of Gems. More Gems to spend, share, or save for any future releases! - The Fall Gala is in full swing so we thought it was the perfect time to introduce our first ever Gem Sale! Now you will get more Gems with each purchase than ever before. + The Fall Gala is in full swing so we thought it was the perfect time for a Gem Sale! Now you will get more Gems with each purchase than ever before. View Gem Bundles Between %s and %s, simply purchase any Gem bundle like usual and your account will be credited with the promotional amount of Gems. More Gems to spend, share, or save for any future releases! This promotion only applies during the limited time event. This event starts on %s (12:00 UTC) and will end %s (00:00 UTC). The promo offer is only available when buying Gems for yourself. @@ -1183,4 +1183,5 @@ Terms of Service %.01f dmg pending %s remaining + Sale ends in %s diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt index 216c0d5a2..27f27e7bc 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt @@ -6,9 +6,11 @@ import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.content.res.Configuration import android.content.res.Resources import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase +import android.os.Build import android.os.Build.VERSION.SDK_INT import android.util.Log import androidx.appcompat.app.AppCompatDelegate @@ -31,6 +33,7 @@ import com.habitrpg.android.habitica.api.HostConfig import com.habitrpg.android.habitica.components.AppComponent import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.ApiClient +import com.habitrpg.android.habitica.helpers.LanguageHelper import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager import com.habitrpg.android.habitica.modules.UserModule @@ -46,6 +49,7 @@ import org.solovyev.android.checkout.Billing import org.solovyev.android.checkout.Cache import org.solovyev.android.checkout.Checkout import org.solovyev.android.checkout.PurchaseVerifier +import java.util.Locale import javax.inject.Inject // contains all HabiticaApplicationLogic except dagger componentInitialisation @@ -78,6 +82,7 @@ abstract class HabiticaBaseApplication : Application() { super.onCreate() setupRealm() setupDagger() + setLocale() setupRemoteConfig() setupNotifications() createBillingAndCheckout() @@ -117,6 +122,21 @@ abstract class HabiticaBaseApplication : Application() { checkIfNewVersion() } + private fun setLocale() { + val resources = resources + val configuration: Configuration = resources.configuration + val languageHelper = LanguageHelper(sharedPrefs.getString("language", "en")) + if (if (SDK_INT >= Build.VERSION_CODES.N) { + configuration.locales.isEmpty || configuration.locales[0] != languageHelper.locale + } else { + configuration.locale != languageHelper.locale + } + ) { + configuration.setLocale(languageHelper.locale) + resources.updateConfiguration(configuration, null) + } + } + protected open fun setupRealm() { Realm.init(this) val builder = RealmConfiguration.Builder() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaPurchaseVerifier.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaPurchaseVerifier.kt index f96c84d9d..5d062fcb8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaPurchaseVerifier.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaPurchaseVerifier.kt @@ -2,6 +2,7 @@ package com.habitrpg.android.habitica import android.content.Context import android.content.SharedPreferences +import androidx.core.content.edit import androidx.preference.PreferenceManager import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -26,32 +27,35 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur private val context: Context override fun doVerify(purchases: List, requestListener: RequestListener>) { val verifiedPurchases: MutableList = ArrayList(purchases.size) + val allPurchases = purchases.toMutableList() for (purchase in purchases) { if (purchasedOrderList.contains(purchase.orderId)) { verifiedPurchases.add(purchase) - requestListener.onSuccess(verifiedPurchases) + processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener) } else { when { PurchaseTypes.allGemTypes.contains(purchase.sku) -> { val validationRequest = buildValidationRequest(purchase) apiClient.validatePurchase(validationRequest).subscribe({ purchasedOrderList.add(purchase.orderId) - requestListener.onSuccess(verifiedPurchases) + verifiedPurchases.add(purchase) + processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener) val giftedID = removeGift(purchase.sku) EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID)) }) { throwable: Throwable -> - handleError(throwable, purchase, requestListener, verifiedPurchases) + handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases) } } PurchaseTypes.allSubscriptionNoRenewTypes.contains(purchase.sku) -> { val validationRequest = buildValidationRequest(purchase) apiClient.validateNoRenewSubscription(validationRequest).subscribe({ purchasedOrderList.add(purchase.orderId) - requestListener.onSuccess(verifiedPurchases) + verifiedPurchases.add(purchase) + processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener) val giftedID = removeGift(purchase.sku) EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID)) }) { throwable: Throwable -> - handleError(throwable, purchase, requestListener, verifiedPurchases) + handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases) } } PurchaseTypes.allSubscriptionTypes.contains(purchase.sku) -> { @@ -63,20 +67,35 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur apiClient.validateSubscription(validationRequest).subscribe({ purchasedOrderList.add(purchase.orderId) verifiedPurchases.add(purchase) - requestListener.onSuccess(verifiedPurchases) + processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener) FirebaseAnalytics.getInstance(context).logEvent("user_subscribed", null) EventBus.getDefault().post(UserSubscribedEvent()) }) { throwable: Throwable -> - handleError(throwable, purchase, requestListener, verifiedPurchases) + handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases) } } } } } - val edit = preferences?.edit() - edit?.putStringSet(PURCHASED_PRODUCTS_KEY, purchasedOrderList) - edit?.apply() - savePendingGifts() + preferences?.edit { + putStringSet(PURCHASED_PRODUCTS_KEY, purchasedOrderList) + } + } + + private fun processedPurchase( + purchase: Purchase, + allPurchases: MutableList, + verifiedPurchases: MutableList, + requestListener: RequestListener> + ) { + allPurchases.remove(purchase) + if (allPurchases.isEmpty()) { + if (verifiedPurchases.isEmpty()) { + requestListener.onError(ResponseCodes.ERROR, Exception()) + } else { + requestListener.onSuccess(verifiedPurchases) + } + } } private fun buildValidationRequest(purchase: Purchase): PurchaseValidationRequest { @@ -92,13 +111,17 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur return validationRequest } - private fun handleError(throwable: Throwable, purchase: Purchase, requestListener: RequestListener>, verifiedPurchases: MutableList) { + private fun handleError(throwable: Throwable, purchase: Purchase, + allPurchases: MutableList, + requestListener: RequestListener>, + verifiedPurchases: MutableList) { (throwable as? HttpException)?.let { error -> if (error.code() == 401) { val res = apiClient.getErrorResponse(throwable) if (res.message != null && res.message == "RECEIPT_ALREADY_USED") { purchasedOrderList.add(purchase.orderId) - requestListener.onSuccess(verifiedPurchases) + verifiedPurchases.add(purchase) + processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener) EventBus.getDefault().post(ConsumablePurchasedEvent(purchase)) removeGift(purchase.sku) return @@ -106,7 +129,7 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur } } FirebaseCrashlytics.getInstance().recordException(throwable) - requestListener.onError(ResponseCodes.ERROR, Exception()) + processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener) } private fun loadPendingGifts(): MutableMap { @@ -131,6 +154,7 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur private const val PENDING_GIFTS_KEY = "PENDING_GIFTS" private var pendingGifts: MutableMap = HashMap() private var preferences: SharedPreferences? = null + fun addGift(sku: String?, userID: String?) { pendingGifts[sku] = userID savePendingGifts() @@ -145,10 +169,9 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur private fun savePendingGifts() { val jsonObject = JSONObject(pendingGifts as Map<*, *>) val jsonString = jsonObject.toString() - val editor = preferences?.edit() - editor?.remove(PENDING_GIFTS_KEY) - editor?.putString(PENDING_GIFTS_KEY, jsonString) - editor?.apply() + preferences?.edit { + putString(PENDING_GIFTS_KEY, jsonString) + } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt index 795d5463c..4c9a92965 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt @@ -218,7 +218,7 @@ interface ApiService { fun seenMessages(@Path("gid") groupId: String): Flowable> @POST("groups/{gid}/invite") - fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map): Flowable> + fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map): Flowable>> @POST("groups/{gid}/reject-invite") fun rejectGroupInvite(@Path("gid") groupId: String): Flowable> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java b/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java index f62177ff5..c24273044 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java @@ -103,6 +103,7 @@ import com.habitrpg.android.habitica.ui.fragments.tasks.TeamBoardFragment; import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel; import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModel; import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel; +import com.habitrpg.android.habitica.ui.viewmodels.inventory.equipment.EquipmentOverviewViewModel; import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog; import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog; import com.habitrpg.android.habitica.ui.views.social.ChatBarView; @@ -346,4 +347,6 @@ public interface UserComponent { void inject(@NotNull PromoWebFragment promoWebFragment); void inject(@NotNull ItemDialogFragment itemDialogFragment); + + void inject(@NotNull EquipmentOverviewViewModel equipmentOverviewViewModel); } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt index 88586a755..c3a7eed58 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt @@ -153,7 +153,7 @@ interface ApiClient { fun seenMessages(groupId: String): Flowable - fun inviteToGroup(groupId: String, inviteData: Map): Flowable + fun inviteToGroup(groupId: String, inviteData: Map): Flowable> fun rejectGroupInvite(groupId: String): Flowable diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt index 61da77a81..b0c0ef49a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt @@ -51,7 +51,7 @@ interface SocialRepository : BaseRepository { fun getGroupMembers(id: String): Flowable> fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable> - fun inviteToGroup(id: String, inviteData: Map): Flowable + fun inviteToGroup(id: String, inviteData: Map): Flowable> fun getMember(userId: String?): Flowable fun getMemberWithUsername(username: String?): Flowable @@ -60,6 +60,8 @@ interface SocialRepository : BaseRepository { fun markPrivateMessagesRead(user: User?): Flowable + fun markSomePrivateMessagesAsRead(user: User?, messages: List) + fun transferGroupOwnership(groupID: String, userID: String): Flowable fun removeMemberFromGroup(groupID: String, userID: String): Flowable> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt index c6d60fcd5..942b46c64 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt @@ -12,7 +12,6 @@ import com.habitrpg.android.habitica.api.GSonFactoryCreator import com.habitrpg.android.habitica.api.HostConfig import com.habitrpg.android.habitica.api.Server import com.habitrpg.android.habitica.data.ApiClient -import com.habitrpg.android.habitica.events.ConsumablePurchasedEvent import com.habitrpg.android.habitica.events.ShowConnectionProblemEvent import com.habitrpg.android.habitica.helpers.NotificationsManager import com.habitrpg.android.habitica.models.* @@ -554,7 +553,7 @@ class ApiClientImpl // private OnHabitsAPIResult mResultListener; return apiService.seenMessages(groupId).compose(configureApiCallObserver()) } - override fun inviteToGroup(groupId: String, inviteData: Map): Flowable { + override fun inviteToGroup(groupId: String, inviteData: Map): Flowable> { return apiService.inviteToGroup(groupId, inviteData).compose(configureApiCallObserver()) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt index c58fabf92..c726a01e9 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt @@ -241,7 +241,7 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap .doOnNext { members -> localRepository.saveGroupMembers(id, members) } } - override fun inviteToGroup(id: String, inviteData: Map): Flowable = apiClient.inviteToGroup(id, inviteData) + override fun inviteToGroup(id: String, inviteData: Map): Flowable> = apiClient.inviteToGroup(id, inviteData) override fun getMember(userId: String?): Flowable { return if (userId == null) { @@ -264,12 +264,31 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap } override fun markPrivateMessagesRead(user: User?): Flowable { + if (user?.isManaged == true) { + localRepository.modify(user) { + it.inbox?.hasUserSeenInbox = true + } + } return apiClient.markPrivateMessagesRead() - .doOnNext { - if (user?.isManaged == true) { - localRepository.modify(user) { it.inbox?.newMessages = 0 } + } + + override fun markSomePrivateMessagesAsRead(user: User?, messages: List) { + if (user?.isManaged == true) { + val numOfUnseenMessages = messages.count { !it.isSeen } + localRepository.modify(user) { + val numOfNewMessagesFromInbox = it.inbox?.newMessages ?: 0 + if (numOfNewMessagesFromInbox > numOfUnseenMessages) { + it.inbox?.newMessages = numOfNewMessagesFromInbox - numOfUnseenMessages + } else { + it.inbox?.newMessages = 0 } } + } + for (message in messages.filter { it.isManaged && !it.isSeen }) { + localRepository.modify(message) { + it.isSeen = true + } + } } override fun getUserGroups(type: String?): Flowable> = localRepository.getUserGroups(userID, type) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt index efbced2a6..07329b047 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt @@ -64,6 +64,13 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm) override fun saveInboxMessages(userID: String, recipientID: String, messages: List, page: Int) { messages.forEach { it.userID = userID } + for (message in messages) { + val existingMessage = realm.where(ChatMessage::class.java) + .equalTo("id", message.id) + .findAll() + .firstOrNull() + message.isSeen = existingMessage != null + } save(messages) if (page != 0) return val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).equalTo("uuid", recipientID).findAll() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTagLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTagLocalRepository.kt index 28e0e317d..8f1e93c8c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTagLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmTagLocalRepository.kt @@ -8,11 +8,13 @@ import io.realm.Realm class RealmTagLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TagLocalRepository { override fun deleteTag(tagID: String) { - val tag = realm.where(Tag::class.java).equalTo("id", tagID).findFirst() - executeTransaction { tag?.deleteFromRealm() } + val tags = realm.where(Tag::class.java).equalTo("id", tagID).findAll() + executeTransaction { tags.deleteAllFromRealm() } } override fun getTags(userId: String): Flowable> { - return RxJavaBridge.toV3Flowable(realm.where(Tag::class.java).equalTo("userId", userId).findAll().asFlowable()) + return RxJavaBridge.toV3Flowable( + realm.where(Tag::class.java).equalTo("userId", userId).findAll().asFlowable() + ) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmUserLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmUserLocalRepository.kt index 3d9a8a4e7..455c2b00d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmUserLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmUserLocalRepository.kt @@ -90,6 +90,17 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), } } executeTransaction { realm1 -> realm1.insertOrUpdate(user) } + removeOldTags(user.id ?: "", user.tags) + } + + private fun removeOldTags(userId: String, onlineTags: List) { + val tags = realm.where(Tag::class.java).equalTo("userId", userId).findAll().createSnapshot() + val tagsToDelete = tags.filterNot { onlineTags.contains(it) } + executeTransaction { + for (tag in tagsToDelete) { + tag.deleteFromRealm() + } + } } override fun saveMessages(messages: List) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt index 8965115dd..8f0195f5d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt @@ -126,6 +126,9 @@ class AppConfigManager(contentRepository: ContentRepository?) { if (promo == null && remoteConfig.getString("activePromo").isNotBlank()) { promo = getHabiticaPromotionFromKey(remoteConfig.getString("activePromo"), null, null) } + if (promo?.isActive != true) { + return null + } if (promo is HabiticaWebPromotion) { promo.url = surveyURL() } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt index 45a415a1e..65fb1736b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt @@ -4,6 +4,8 @@ import android.app.Activity import android.content.Intent import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.proxy.AnalyticsManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.solovyev.android.checkout.* import java.util.* import kotlin.coroutines.resume @@ -74,16 +76,18 @@ open class PurchaseHandler(activity: Activity, val analyticsManager: AnalyticsMa return purchases.skus.firstOrNull() } - private suspend fun loadInventory(type: String, skus: List): Inventory.Products? = suspendCoroutine { cont -> - val request = Inventory.Request.create().loadAllPurchases().loadSkus(type, skus) - try { - inventory?.load(request) { - cont.resume(it) + private suspend fun loadInventory(type: String, skus: List): Inventory.Products? = withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + val request = Inventory.Request.create().loadAllPurchases().loadSkus(type, skus) + try { + inventory?.load(request) { + cont.resume(it) + } + } catch (e: NullPointerException) { + cont.resumeWithException(e) } - } catch (e: NullPointerException) { - cont.resumeWithException(e) + if (inventory == null) cont.resume(null) } - if (inventory == null) cont.resume(null) } fun purchaseSubscription(sku: Sku, onSuccess: (() -> Unit)) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/SoundManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/SoundManager.kt index 940ff3765..d4387f280 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/SoundManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/SoundManager.kt @@ -1,7 +1,6 @@ package com.habitrpg.android.habitica.helpers import com.habitrpg.android.habitica.HabiticaBaseApplication -import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject @@ -17,9 +16,10 @@ class SoundManager { HabiticaBaseApplication.userComponent?.inject(this) } - fun preloadAllFiles(): Maybe> { + fun preloadAllFiles() { + loadedSoundFiles.clear() if (soundTheme == SoundThemeOff) { - return Maybe.empty() + return } val soundFiles = ArrayList() @@ -33,7 +33,8 @@ class SoundManager { soundFiles.add(SoundFile(soundTheme, SoundPlusHabit)) soundFiles.add(SoundFile(soundTheme, SoundReward)) soundFiles.add(SoundFile(soundTheme, SoundTodo)) - return soundFileLoader.download(soundFiles).toMaybe() + soundFileLoader.download(soundFiles) + .subscribe({}, RxErrorHandler.handleEmptyError()) } fun loadAndPlayAudio(type: String) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt index 2b80cb16f..2b4db0b6e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt @@ -1,5 +1,6 @@ package com.habitrpg.android.habitica.helpers.notifications +import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.content.Context @@ -40,6 +41,7 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic oldMessages.add(data) return super.configureNotificationBuilder(data) .setStyle(style) + .setCategory(Notification.CATEGORY_MESSAGE) .setExtras(bundleOf(Pair("messages", bundleOf(Pair("messages", oldMessages))))) } @@ -55,8 +57,8 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic ) } - override fun setNotificationActions(data: Map) { - super.setNotificationActions(data) + override fun setNotificationActions(notificationId: Int, data: Map) { + super.setNotificationActions(notificationId, data) val groupID = data["groupID"] ?: return val actionName = context.getString(R.string.group_message_reply) @@ -68,6 +70,7 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic val intent = Intent(context, LocalNotificationActionReceiver::class.java) intent.action = actionName intent.putExtra("groupID", groupID) + intent.putExtra("NOTIFICATION_ID", notificationId) val replyPendingIntent: PendingIntent = PendingIntent.getBroadcast( context, groupID.hashCode(), diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt index e9e3f2ecc..80c5e0421 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt @@ -16,16 +16,18 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi intent.putExtra("groupID", data?.get("groupID")) } - override fun setNotificationActions(data: Map) { - super.setNotificationActions(data) + override fun setNotificationActions(notificationId: Int, data: Map) { + super.setNotificationActions(notificationId, data) val res = context.resources val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) acceptInviteIntent.action = res.getString(R.string.accept_guild_invite) - acceptInviteIntent.putExtra("groupID", this.data?.get("groupID")) + val groupID = data.get("groupID") + acceptInviteIntent.putExtra("groupID", groupID) + acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntentAccept = PendingIntent.getBroadcast( context, - 3000, + groupID.hashCode(), acceptInviteIntent, PendingIntent.FLAG_UPDATE_CURRENT ) @@ -33,10 +35,11 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) rejectInviteIntent.action = res.getString(R.string.reject_guild_invite) - rejectInviteIntent.putExtra("groupID", this.data?.get("groupID")) + rejectInviteIntent.putExtra("groupID", groupID) + acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntentReject = PendingIntent.getBroadcast( context, - 2000, + groupID.hashCode() + 1, rejectInviteIntent, PendingIntent.FLAG_UPDATE_CURRENT ) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt index b85a6369e..f44f7d0b8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt @@ -44,19 +44,21 @@ abstract class HabiticaLocalNotification(protected var context: Context, protect notificationBuilder = notificationBuilder.setContentText(message) } - this.setNotificationActions(data) + val notificationId = getNotificationID(data) + this.setNotificationActions(notificationId, data) val notificationManager = NotificationManagerCompat.from(context) - notificationManager.notify(getNotificationID(data), notificationBuilder.build()) + notificationManager.notify(notificationId, notificationBuilder.build()) } fun setExtras(data: Map) { this.data = data } - protected open fun setNotificationActions(data: Map) { + protected open fun setNotificationActions(notificationId: Int, data: Map) { val intent = Intent(context, MainActivity::class.java) configureMainIntent(intent) + intent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntent = PendingIntent.getActivity( context, 3000, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt index 14ddb6ebd..21c0cc3f7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt @@ -11,16 +11,18 @@ import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver */ class PartyInviteLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { - override fun setNotificationActions(data: Map) { - super.setNotificationActions(data) + override fun setNotificationActions(notificationId: Int, data: Map) { + super.setNotificationActions(notificationId, data) val res = context.resources val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) acceptInviteIntent.action = res.getString(R.string.accept_party_invite) - acceptInviteIntent.putExtra("groupID", this.data?.get("groupID")) + val groupID = data.get("groupID") + acceptInviteIntent.putExtra("groupID", groupID) + acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntentAccept = PendingIntent.getBroadcast( context, - 3000, + groupID.hashCode(), acceptInviteIntent, PendingIntent.FLAG_UPDATE_CURRENT ) @@ -28,10 +30,11 @@ class PartyInviteLocalNotification(context: Context, identifier: String?) : Habi val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) rejectInviteIntent.action = res.getString(R.string.reject_party_invite) - rejectInviteIntent.putExtra("groupID", this.data?.get("groupID")) + rejectInviteIntent.putExtra("groupID", groupID) + rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntentReject = PendingIntent.getBroadcast( context, - 2000, + groupID.hashCode() + 1, rejectInviteIntent, PendingIntent.FLAG_UPDATE_CURRENT ) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt index 2f347a31a..1c3bb5906 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt @@ -15,15 +15,16 @@ class QuestInviteLocalNotification(context: Context, identifier: String?) : Habi return 1000 } - override fun setNotificationActions(data: Map) { - super.setNotificationActions(data) + override fun setNotificationActions(notificationId: Int, data: Map) { + super.setNotificationActions(notificationId, data) val res = context.resources val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) acceptInviteIntent.action = res.getString(R.string.accept_quest_invite) + acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntentAccept = PendingIntent.getBroadcast( context, - 3000, + 3001, acceptInviteIntent, PendingIntent.FLAG_UPDATE_CURRENT ) @@ -31,9 +32,10 @@ class QuestInviteLocalNotification(context: Context, identifier: String?) : Habi val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) rejectInviteIntent.action = res.getString(R.string.reject_quest_invite) + rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId) val pendingIntentReject = PendingIntent.getBroadcast( context, - 2000, + 2001, rejectInviteIntent, PendingIntent.FLAG_UPDATE_CURRENT ) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt index 1f51863f9..8b8502254 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt @@ -1,5 +1,6 @@ package com.habitrpg.android.habitica.helpers.notifications +import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.content.Context @@ -36,6 +37,7 @@ class ReceivedPrivateMessageLocalNotification(context: Context, identifier: Stri } notification = notification .setContentTitle(notificationTitle) + .setCategory(Notification.CATEGORY_MESSAGE) .setStyle(style) title = null } else { @@ -48,8 +50,8 @@ class ReceivedPrivateMessageLocalNotification(context: Context, identifier: Stri return data["senderName"].hashCode() } - override fun setNotificationActions(data: Map) { - super.setNotificationActions(data) + override fun setNotificationActions(notificationId: Int, data: Map) { + super.setNotificationActions(notificationId, data) val senderID = data["replyTo"] ?: return val actionName = context.getString(R.string.inbox_message_reply) @@ -61,6 +63,7 @@ class ReceivedPrivateMessageLocalNotification(context: Context, identifier: Stri val intent = Intent(context, LocalNotificationActionReceiver::class.java) intent.action = actionName intent.putExtra("senderID", senderID) + intent.putExtra("NOTIFICATION_ID", notificationId) val replyPendingIntent: PendingIntent = PendingIntent.getBroadcast( context, senderID.hashCode(), diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/interactors/NotifyUserUseCase.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/interactors/NotifyUserUseCase.kt index 5187fb51a..7145fe98b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/interactors/NotifyUserUseCase.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/interactors/NotifyUserUseCase.kt @@ -109,28 +109,28 @@ constructor( return textView } - fun getNotificationAndAddStatsToUserAsText(xp: Double, hp: Double, gold: Double, mp: Double): Pair { + fun getNotificationAndAddStatsToUserAsText(xp: Double?, hp: Double?, gold: Double?, mp: Double?): Pair { val builder = SpannableStringBuilder() var displayType = SnackbarDisplayType.NORMAL - if (xp > 0) { - builder.append(" + ").append(xp.round(2).toString()).append(" Exp") + if ((xp ?: 0.0) > 0) { + builder.append(" + ").append(xp?.round(2).toString()).append(" Exp") } if (hp != 0.0) { displayType = SnackbarDisplayType.FAILURE - builder.append(" - ").append(abs(hp.round(2)).toString()).append(" Health") + builder.append(" - ").append(abs(hp?.round(2) ?: 0.0).toString()).append(" Health") } if (gold != 0.0) { - if (gold > 0) { - builder.append(" + ").append(gold.round(2).toString()) - } else if (gold < 0) { + if ((gold ?: 0.0) > 0) { + builder.append(" + ").append(gold?.round(2).toString()) + } else if ((gold ?: 0.0) < 0) { displayType = SnackbarDisplayType.FAILURE - builder.append(" - ").append(abs(gold.round(2)).toString()) + builder.append(" - ").append(abs(gold?.round(2) ?: 0.0).toString()) } builder.append(" Gold") } - if (mp > 0) { - builder.append(" + ").append(mp.round(2).toString()).append(" Mana") + if ((mp ?: 0.0) > 0) { + builder.append(" + ").append(mp?.round(2).toString()).append(" Mana") } return Pair(builder, displayType) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/promotions/HabiticaPromotion.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/promotions/HabiticaPromotion.kt index 0c145f841..149d4362c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/promotions/HabiticaPromotion.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/promotions/HabiticaPromotion.kt @@ -17,6 +17,11 @@ enum class PromoType { } abstract class HabiticaPromotion { + val isActive: Boolean + get() { + val now = Date() + return startDate.before(now) && endDate.after(now) + } abstract val identifier: String abstract val promoType: PromoType diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt index a73d4990d..f7678d441 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt @@ -58,6 +58,8 @@ open class ChatMessage : RealmObject(), BaseMainObject { val formattedUsername: String? get() = if (username != null) "@$username" else null + var isSeen: Boolean = true + fun userLikesMessage(userId: String?): Boolean { return likes?.any { userId == it.id } ?: false } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Inbox.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Inbox.kt index 51620f7d2..9aacccd7d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Inbox.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Inbox.kt @@ -10,4 +10,5 @@ open class Inbox : RealmObject(), BaseObject { var optOut: Boolean = false var blocks: RealmList = RealmList() var newMessages: Int = 0 + var hasUserSeenInbox: Boolean = false } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/LocalNotificationActionReceiver.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/LocalNotificationActionReceiver.kt index b29ce5103..5bd4483ff 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/LocalNotificationActionReceiver.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/LocalNotificationActionReceiver.kt @@ -4,6 +4,9 @@ import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import com.habitrpg.android.habitica.HabiticaBaseApplication @@ -13,37 +16,44 @@ import com.habitrpg.android.habitica.data.SocialRepository import com.habitrpg.android.habitica.data.TaskRepository import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.helpers.RxErrorHandler +import com.habitrpg.android.habitica.interactors.NotifyUserUseCase import com.habitrpg.android.habitica.models.user.User import javax.inject.Inject class LocalNotificationActionReceiver : BroadcastReceiver() { @Inject lateinit var userRepository: UserRepository + @Inject lateinit var socialRepository: SocialRepository + @Inject lateinit var taskRepository: TaskRepository + @Inject lateinit var apiClient: ApiClient private var user: User? = null - private var groupID: String? = null - private var senderID: String? = null + private val groupID: String? + get() = intent?.extras?.getString("groupID") + private val senderID: String? + get() = intent?.extras?.getString("senderID") + private val taskID: String? + get() = intent?.extras?.getString("taskID") private var context: Context? = null private var intent: Intent? = null override fun onReceive(context: Context, intent: Intent) { HabiticaBaseApplication.userComponent?.inject(this) this.intent = intent - groupID = intent.extras?.getString("groupID") - senderID = intent.extras?.getString("senderID") this.context = context handleLocalNotificationAction(intent.action) } private fun handleLocalNotificationAction(action: String?) { - val notificationManager = this.context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager - notificationManager?.cancelAll() + val notificationManager = + this.context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager + notificationManager?.cancel(intent?.extras?.getInt("NOTIFICATION_ID") ?: -1) when (action) { context?.getString(R.string.accept_party_invite) -> { groupID?.let { @@ -52,7 +62,8 @@ class LocalNotificationActionReceiver : BroadcastReceiver() { } context?.getString(R.string.reject_party_invite) -> { groupID?.let { - socialRepository.rejectGroupInvite(it).subscribe({ }, RxErrorHandler.handleEmptyError()) + socialRepository.rejectGroupInvite(it) + .subscribe({ }, RxErrorHandler.handleEmptyError()) } } context?.getString(R.string.accept_quest_invite) -> { @@ -68,7 +79,8 @@ class LocalNotificationActionReceiver : BroadcastReceiver() { } context?.getString(R.string.reject_guild_invite) -> { groupID?.let { - socialRepository.rejectGroupInvite(it).subscribe({ }, RxErrorHandler.handleEmptyError()) + socialRepository.rejectGroupInvite(it) + .subscribe({ }, RxErrorHandler.handleEmptyError()) } } context?.getString(R.string.group_message_reply) -> { @@ -76,7 +88,9 @@ class LocalNotificationActionReceiver : BroadcastReceiver() { getMessageText(context?.getString(R.string.group_message_reply))?.let { message -> socialRepository.postGroupChat(it, message).subscribe( { - context?.let { c -> NotificationManagerCompat.from(c).cancel(it.hashCode()) } + context?.let { c -> + NotificationManagerCompat.from(c).cancel(it.hashCode()) + } }, RxErrorHandler.handleEmptyError() ) @@ -86,19 +100,33 @@ class LocalNotificationActionReceiver : BroadcastReceiver() { context?.getString(R.string.inbox_message_reply) -> { senderID?.let { getMessageText(context?.getString(R.string.inbox_message_reply))?.let { message -> - socialRepository.postPrivateMessage(it, message).subscribe({ }, RxErrorHandler.handleEmptyError()) + socialRepository.postPrivateMessage(it, message) + .subscribe({ }, RxErrorHandler.handleEmptyError()) } } } context?.getString(R.string.complete_task_action) -> { - intent?.extras?.getString("taskID")?.let { + taskID?.let { taskRepository.taskChecked(null, it, up = true, force = false) { - }.subscribe({}, RxErrorHandler.handleEmptyError()) + }.subscribe({ + val pair = NotifyUserUseCase.getNotificationAndAddStatsToUserAsText( + it?.experienceDelta, + it?.healthDelta, + it?.goldDelta, + it?.manaDelta + ) + showToast(pair.first) + }, RxErrorHandler.handleEmptyError()) } } } } + private fun showToast(text: Spannable) { + val toast = Toast.makeText(context, text, Toast.LENGTH_LONG) + toast.show() + } + private fun getMessageText(key: String?): String? { return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(key)?.toString() } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt index 85743da84..2425fd7f9 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt @@ -1,10 +1,12 @@ package com.habitrpg.android.habitica.receivers +import android.app.Notification import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.media.RingtoneManager +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.habitrpg.android.habitica.HabiticaBaseApplication @@ -62,21 +64,29 @@ class TaskReceiver : BroadcastReceiver() { val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0) val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - val notificationBuilder = NotificationCompat.Builder(context, "default") + var notificationBuilder = NotificationCompat.Builder(context, "default") .setSmallIcon(R.drawable.ic_gryphon_white) .setContentTitle(task.text) + .setStyle(NotificationCompat.BigTextStyle() + .bigText(task.notes)) .setPriority(NotificationCompat.PRIORITY_MAX) .setSound(soundUri) .setAutoCancel(true) .setContentIntent(pendingIntent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + notificationBuilder = notificationBuilder.setCategory(Notification.CATEGORY_REMINDER) + } + if (task.type == Task.TYPE_DAILY || task.type == Task.TYPE_TODO) { - val completeIntent = Intent(context, LocalNotificationActionReceiver::class.java) - completeIntent.action = context.getString(R.string.complete_task_action) - completeIntent.putExtra("taskID", task.id) + val completeIntent = Intent(context, LocalNotificationActionReceiver::class.java).apply { + action = context.getString(R.string.complete_task_action) + putExtra("taskID", task.id) + putExtra("NOTIFICATION_ID", task.id.hashCode()) + } val pendingIntentComplete = PendingIntent.getBroadcast( context, - 3000, + task.id.hashCode(), completeIntent, PendingIntent.FLAG_UPDATE_CURRENT ) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarView.kt index 5572b178f..b3166efa8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarView.kt @@ -12,6 +12,7 @@ import androidx.core.view.marginStart import androidx.core.view.marginTop import coil.clear import coil.load +import coil.target.ImageViewTarget import com.habitrpg.android.habitica.BuildConfig import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.extensions.dpToPx @@ -143,17 +144,15 @@ class AvatarView : FrameLayout { imageView.load(DataBindingUtils.BASE_IMAGE_URL + DataBindingUtils.getFullFilename(layerName)) { allowHardware(false) - target( - {}, - { + target(object : ImageViewTarget(imageView) { + override fun onError(error: Drawable?) { + super.onError(error) onLayerComplete() - }, - { - if (imageView.tag != layerName) { - return@target - } - val bounds = getLayerBounds(layerKey, layerName, it) - imageView.setImageDrawable(it) + } + + override fun onSuccess(result: Drawable) { + super.onSuccess(result) + val bounds = getLayerBounds(layerKey, layerName, result) imageView.imageMatrix = avatarMatrix val layoutParams = imageView.layoutParams as? LayoutParams layoutParams?.topMargin = bounds.top @@ -163,7 +162,7 @@ class AvatarView : FrameLayout { imageView.layoutParams = layoutParams onLayerComplete() } - ) + }) } } while (i < (imageViewHolder.size)) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt index 62a584a0d..335d7ff5f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BaseActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration import android.content.res.Resources +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -61,7 +62,7 @@ abstract class BaseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val languageHelper = LanguageHelper(sharedPreferences.getString("language", "en")) - resources.forceLocale(languageHelper.locale) + resources.forceLocale(this, languageHelper.locale) delegate.localNightMode = when (sharedPreferences.getString("theme_mode", "system")) { "light" -> AppCompatDelegate.MODE_NIGHT_NO "dark" -> AppCompatDelegate.MODE_NIGHT_YES @@ -77,6 +78,13 @@ abstract class BaseActivity : AppCompatActivity() { compositeSubscription = CompositeDisposable() } + override fun onRestart() { + super.onRestart() + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + val languageHelper = LanguageHelper(sharedPreferences.getString("language", "en")) + resources.forceLocale(this, languageHelper.locale) + } + override fun onStart() { super.onStart() EventBus.getDefault().register(this) @@ -222,9 +230,12 @@ abstract class BaseActivity : AppCompatActivity() { } } -private fun Resources.forceLocale(locale: Locale) { +private fun Resources.forceLocale(activity: BaseActivity, locale: Locale) { Locale.setDefault(locale) val configuration = Configuration() configuration.setLocale(locale) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + activity.createConfigurationContext(configuration) + } updateConfiguration(configuration, displayMetrics) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt index d7092d3d6..a1f9e4625 100755 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt @@ -847,7 +847,7 @@ open class MainActivity : BaseActivity(), TutorialView.OnTutorialReaction { } @Subscribe - fun showWonAchievementDialog(event: ShowWonChallengeDialog) { + fun showWonChallengeDialog(event: ShowWonChallengeDialog) { retrieveUser(true) lifecycleScope.launch(context = Dispatchers.Main) { val dialog = WonChallengeDialog(this@MainActivity) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/PrefsActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/PrefsActivity.kt index 8cfa2adde..b468fe561 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/PrefsActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/PrefsActivity.kt @@ -20,7 +20,7 @@ class PrefsActivity : BaseActivity(), PreferenceFragmentCompat.OnPreferenceStart setupToolbar(findViewById(R.id.toolbar)) supportFragmentManager.beginTransaction() - .add(R.id.fragment_container, PreferencesFragment()) + .replace(R.id.fragment_container, PreferencesFragment()) .commit() } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt index 485918e97..a1b694030 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt @@ -16,6 +16,7 @@ import androidx.core.os.bundleOf import androidx.core.view.GravityCompat import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.SimpleItemAnimator import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.ContentRepository @@ -28,12 +29,14 @@ import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.MainNavigationController import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.models.WorldState +import com.habitrpg.android.habitica.models.WorldStateEvent import com.habitrpg.android.habitica.models.inventory.Item import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion import com.habitrpg.android.habitica.models.promotions.PromoType import com.habitrpg.android.habitica.models.social.Group +import com.habitrpg.android.habitica.models.user.Inbox import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.activities.MainActivity import com.habitrpg.android.habitica.ui.activities.NotificationsActivity @@ -98,6 +101,7 @@ class NavigationDrawerFragment : DialogFragment() { private fun updateQuestDisplay() { val quest = this.quest val questContent = this.questContent + return if (quest == null || questContent == null || !quest.active) { binding?.questMenuView?.visibility = View.GONE context?.let { @@ -125,20 +129,6 @@ class NavigationDrawerFragment : DialogFragment() { } binding?.questMenuView?.setBackgroundColor(context?.getThemeColor(R.attr.colorPrimaryDark) ?: 0) - /* Reenable this once the boss art can be displayed correctly. - - val preferences = context?.getSharedPreferences("collapsible_sections", 0) - if (preferences?.getBoolean("boss_art_collapsed", false) == true) { - questMenuView.hideBossArt() - } else { - questMenuView.showBossArt() - }*/ - //binding?.questMenuView?.hideBossArt() - - /*getItemWithIdentifier(SIDEBAR_TAVERN)?.let { tavern -> - tavern.subtitle = context?.getString(R.string.active_world_boss) - adapter.updateItem(tavern) - }*/ binding?.questMenuView?.setOnClickListener { setSelection(R.id.partyFragment) /*val context = this.context @@ -181,6 +171,7 @@ class NavigationDrawerFragment : DialogFragment() { binding?.recyclerView?.adapter = adapter binding?.recyclerView?.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + (binding?.recyclerView?.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false initializeMenuItems() subscriptions?.add( @@ -237,12 +228,12 @@ class NavigationDrawerFragment : DialogFragment() { { pair -> val gearEvent = pair.first.events.firstOrNull { it.gear } createUpdatingJob("seasonal", { - gearEvent?.end?.after(Date()) == true || pair.second.isNotEmpty() + gearEvent?.isCurrentlyActive == true || pair.second.isNotEmpty() }, { val diff = (gearEvent?.end?.time ?: 0) - Date().time if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1) }) { - updateSeasonalMenuEntries(pair.first, pair.second) + updateSeasonalMenuEntries(gearEvent, pair.second) } }, RxErrorHandler.handleEmptyError() @@ -278,6 +269,7 @@ class NavigationDrawerFragment : DialogFragment() { it.quest?.key ?: "" } .flatMapMaybe { inventoryRepository.getQuestContent(it).firstElement() } + .filter { (it.boss?.hp ?: 0) > 0 } .subscribe( { questContent = it @@ -310,7 +302,7 @@ class NavigationDrawerFragment : DialogFragment() { } } - private fun updateSeasonalMenuEntries(worldState: WorldState, items: List) { + private fun updateSeasonalMenuEntries(gearEvent: WorldStateEvent?, items: List) { val market = getItemWithIdentifier(SIDEBAR_SHOPS_MARKET) ?: return if (items.isNotEmpty() && items.firstOrNull()?.event?.end?.after(Date()) == true) { market.pillText = context?.getString(R.string.something_new) @@ -323,8 +315,7 @@ class NavigationDrawerFragment : DialogFragment() { val shop = getItemWithIdentifier(SIDEBAR_SHOPS_SEASONAL) ?: return shop.pillText = context?.getString(R.string.open) - val gearEvent = worldState.events.firstOrNull { it.gear } - if (gearEvent?.end?.after(Date()) == true) { + if (gearEvent?.isCurrentlyActive == true) { shop.isVisible = true shop.subtitle = context?.getString(R.string.open_for, gearEvent.end?.getShortRemainingString()) } else { @@ -334,7 +325,7 @@ class NavigationDrawerFragment : DialogFragment() { } private fun updateUser(user: User) { - setMessagesCount(user.inbox?.newMessages ?: 0) + setMessagesCount(user.inbox) setSettingsCount(if (user.flags?.verifiedUsername != true) 1 else 0) setDisplayName(user.profile?.name) setUsername(user.formattedUsername) @@ -629,12 +620,23 @@ class NavigationDrawerFragment : DialogFragment() { } } - private fun setMessagesCount(unreadMessages: Int) { - if (unreadMessages == 0) { - binding?.messagesBadge?.visibility = View.GONE - } else { + private fun setMessagesCount(inbox: Inbox?) { + val numOfUnreadMessages = inbox?.newMessages ?: 0 + if (numOfUnreadMessages != 0) { binding?.messagesBadge?.visibility = View.VISIBLE - binding?.messagesBadge?.text = unreadMessages.toString() + binding?.messagesBadge?.text = numOfUnreadMessages.toString() + context?.let { + val color = if (inbox?.hasUserSeenInbox != true) { + it.getThemeColor(R.attr.colorAccent) + } else { + ContextCompat.getColor(it, R.color.gray_200) + } + val background = binding?.messagesBadge?.background as? GradientDrawable + background?.color = ColorStateList.valueOf(color) + binding?.messagesBadge?.setTextColor(ContextCompat.getColor(it, R.color.white)) + } + } else { + binding?.messagesBadge?.visibility = View.GONE } } @@ -652,13 +654,9 @@ class NavigationDrawerFragment : DialogFragment() { activePromo = configManager.activePromo() val promoItem = getItemWithIdentifier(SIDEBAR_PROMO) ?: return activePromo?.let { activePromo -> - if (sharedPreferences.getBoolean("hide${activePromo.identifier}", false)) { - promoItem.isVisible = true - adapter.activePromo = activePromo - } else { - promoItem.isVisible = false - } - + promoItem.isVisible = + !sharedPreferences.getBoolean("hide${activePromo.identifier}", false) + adapter.activePromo = activePromo var promotedItem: HabiticaDrawerItem? = null if (activePromo.promoType == PromoType.GEMS_AMOUNT || activePromo.promoType == PromoType.GEMS_PRICE) { promotedItem = getItemWithIdentifier(SIDEBAR_GEMS) @@ -670,13 +668,18 @@ class NavigationDrawerFragment : DialogFragment() { promotedItem.pillText = context?.getString(R.string.sale) promotedItem.pillBackground = context?.let { activePromo.pillBackgroundDrawable(it) } createUpdatingJob(activePromo.promoType.name, { - activePromo.endDate.after(Date()) + activePromo.isActive }, { val diff = activePromo.endDate.time - Date().time if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1) }) { - promotedItem.subtitle = context?.getString(R.string.x_remaining, activePromo.endDate.getShortRemainingString()) - updateItem(promotedItem) + if (activePromo.isActive) { + promotedItem.subtitle = context?.getString(R.string.sale_ends_in, activePromo.endDate.getShortRemainingString()) + updateItem(promotedItem) + } else { + promotedItem.subtitle = null + updateItem(promotedItem) + } } } ?: run { promoItem.isVisible = false diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt index 1ee163b59..665316463 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentDetailFragment.kt @@ -33,7 +33,6 @@ class EquipmentDetailFragment : } var type: String? = null - var typeText: String? = null var equippedGear: String? = null var isCostume: Boolean? = null diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt index 45101cbb1..2d0fb80de 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt @@ -4,38 +4,26 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.viewModels import com.habitrpg.android.habitica.components.UserComponent -import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.databinding.FragmentEquipmentOverviewBinding import com.habitrpg.android.habitica.helpers.MainNavigationController -import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.models.user.Gear -import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.models.user.Outfit import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment -import javax.inject.Inject +import com.habitrpg.android.habitica.ui.viewmodels.inventory.equipment.EquipmentOverviewViewModel +import com.habitrpg.android.habitica.ui.views.equipment.EquipmentOverviewView class EquipmentOverviewFragment : BaseMainFragment() { + private val viewModel: EquipmentOverviewViewModel by viewModels() + override var binding: FragmentEquipmentOverviewBinding? = null override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentEquipmentOverviewBinding { return FragmentEquipmentOverviewBinding.inflate(inflater, container, false) } - @Inject - lateinit var inventoryRepository: InventoryRepository - - override var user: User? - get() = super.user - set(value) { - super.user = value - if (this::inventoryRepository.isInitialized) { - value?.items?.gear?.let { - updateGearData(it) - } - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -46,22 +34,24 @@ class EquipmentOverviewFragment : BaseMainFragment + if (isChecked == viewModel.user.value?.preferences?.autoEquip) return@setOnCheckedChangeListener + viewModel.updateUser("preferences.autoEquip", isChecked) } + binding?.costumeSwitch?.setOnCheckedChangeListener { _, isChecked -> + if (isChecked == viewModel.user.value?.preferences?.costume) return@setOnCheckedChangeListener + viewModel.updateUser("preferences.costume", isChecked) } - binding?.autoEquipSwitch?.setOnCheckedChangeListener { _, isChecked -> userRepository.updateUser("preferences.autoEquip", isChecked).subscribe({ }, RxErrorHandler.handleEmptyError()) } - binding?.costumeSwitch?.setOnCheckedChangeListener { _, isChecked -> userRepository.updateUser("preferences.costume", isChecked).subscribe({ }, RxErrorHandler.handleEmptyError()) } + viewModel.user.observe(viewLifecycleOwner) { + it?.items?.gear?.let { + updateGearData(it) + } + binding?.autoEquipSwitch?.isChecked = user?.preferences?.autoEquip ?: false + binding?.costumeSwitch?.isChecked = user?.preferences?.costume ?: false - user?.items?.gear?.let { - updateGearData(it) + binding?.costumeView?.isEnabled = user?.preferences?.costume == true } } - override fun onDestroy() { - inventoryRepository.close() - super.onDestroy() - } - override fun injectFragment(component: UserComponent) { component.inject(this) } @@ -71,31 +61,17 @@ class EquipmentOverviewFragment : BaseMainFragment(), GemPurchaseActivity.CheckoutFragment { @@ -98,8 +99,10 @@ class GemsPurchaseFragment : BaseFragment(), GemPurc override fun setupCheckout() { CoroutineScope(Dispatchers.IO).launch { val skus = purchaseHandler?.getAllGemSKUs() - for (sku in skus ?: emptyList()) { - updateButtonLabel(sku.id.code, sku.price) + withContext(Dispatchers.Main) { + for (sku in skus ?: emptyList()) { + updateButtonLabel(sku.id.code, sku.price) + } } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GiftPurchaseGemsFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GiftPurchaseGemsFragment.kt index d8f400fb5..d77903a25 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GiftPurchaseGemsFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GiftPurchaseGemsFragment.kt @@ -18,6 +18,7 @@ import com.habitrpg.android.habitica.ui.fragments.BaseFragment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject class GiftPurchaseGemsFragment : BaseFragment() { @@ -63,8 +64,10 @@ class GiftPurchaseGemsFragment : BaseFragment() fun setupCheckout() { CoroutineScope(Dispatchers.IO).launch { val skus = purchaseHandler?.getAllGemSKUs() - for (sku in skus ?: emptyList()) { - updateButtonLabel(sku.id.code, sku.price) + withContext(Dispatchers.Main) { + for (sku in skus ?: emptyList()) { + updateButtonLabel(sku.id.code, sku.price) + } } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionFragment.kt index ba4a1381d..d35565f89 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionFragment.kt @@ -35,6 +35,7 @@ import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionVi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.solovyev.android.checkout.Inventory import org.solovyev.android.checkout.Purchase @@ -145,12 +146,15 @@ class SubscriptionFragment : BaseFragment(), GemPur override fun setupCheckout() { CoroutineScope(Dispatchers.IO).launch { val subscriptions = purchaseHandler?.getAllSubscriptionProducts() ?: return@launch - for (sku in subscriptions.skus) { - updateButtonLabel(sku, sku.price, subscriptions) + skus = subscriptions.skus + withContext(Dispatchers.Main) { + for (sku in subscriptions.skus) { + updateButtonLabel(sku, sku.price, subscriptions) + } + selectSubscription(PurchaseTypes.Subscription1Month) + hasLoadedSubscriptionOptions = true + updateSubscriptionInfo() } - selectSubscription(PurchaseTypes.Subscription1Month) - hasLoadedSubscriptionOptions = true - updateSubscriptionInfo() } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt index 610056e76..1086892e0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt @@ -101,7 +101,7 @@ class ChatFragment : BaseFragment() { viewModel?.updateUser("flags.communityGuidelinesAccepted", true) } - viewModel?.getUserData()?.observe( + viewModel?.user?.observe( viewLifecycleOwner, { chatAdapter?.user = it diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt index 6e189dfd1..5603a0ca5 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt @@ -88,7 +88,12 @@ class InboxMessageListFragment : BaseMainFragment) { + socialRepository.markSomePrivateMessagesAsRead(user, messages) + } + private fun startAutoRefreshing() { if (refreshDisposable != null && refreshDisposable?.isDisposed != true) { refreshDisposable?.dispose() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt index 5d2de4c30..093539c71 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt @@ -111,21 +111,22 @@ class GuildDetailFragment : BaseFragment() { private val sendInvitesResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { val inviteData = HashMap() - inviteData["inviter"] = viewModel?.getUserData()?.value?.profile?.name ?: "" - if (it.data?.getBooleanExtra(GroupInviteActivity.IS_EMAIL_KEY, false) == true) { - val emails = it.data?.getStringArrayExtra(GroupInviteActivity.EMAILS_KEY) + inviteData["inviter"] = viewModel?.user?.value?.profile?.name ?: "" + val emails = it.data?.getStringArrayExtra(GroupInviteActivity.EMAILS_KEY) + if (emails != null && emails.isNotEmpty()) { val invites = ArrayList>() - emails?.forEach { email -> + emails.forEach { email -> val invite = HashMap() invite["name"] = "" invite["email"] = email invites.add(invite) } inviteData["emails"] = invites - } else { - val userIDs = it.data?.getStringArrayExtra(GroupInviteActivity.USER_IDS_KEY) - val invites = mutableListOf() - userIDs?.forEach { invites.add(it) } + } + val userIDs = it.data?.getStringArrayExtra(GroupInviteActivity.USER_IDS_KEY) + if (userIDs != null && userIDs.isNotEmpty()) { + val invites = ArrayList() + userIDs.forEach { invites.add(it) } inviteData["usernames"] = invites } viewModel?.inviteToGroup(inviteData) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt index 8dfdb6ec6..1ca9c25bf 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt @@ -114,7 +114,7 @@ class PartyDetailFragment : BaseFragment() { } viewModel?.getGroupData()?.observe(viewLifecycleOwner, { updateParty(it) }) - viewModel?.getUserData()?.observe(viewLifecycleOwner, { updateUser(it) }) + viewModel?.user?.observe(viewLifecycleOwner, { updateUser(it) }) viewModel?.getMembersData()?.observe(viewLifecycleOwner, { updateMembersList(it) }) } @@ -269,7 +269,7 @@ class PartyDetailFragment : BaseFragment() { } ) ?: return@forEachIndexed val viewHolder = GroupMemberViewHolder(memberView) - viewHolder.bind(member, leaderID ?: "", viewModel?.getUserData()?.value?.id) + viewHolder.bind(member, leaderID ?: "", viewModel?.user?.value?.id) viewHolder.onClickEvent = { FullProfileActivity.open(member.id ?: "") } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/BaseViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/BaseViewModel.kt index 32417f81d..23551b00d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/BaseViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/BaseViewModel.kt @@ -7,6 +7,7 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.helpers.RxErrorHandler +import com.habitrpg.android.habitica.models.inventory.Equipment import com.habitrpg.android.habitica.models.user.User import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -17,10 +18,13 @@ abstract class BaseViewModel(initializeComponent: Boolean = true) : ViewModel() @Inject lateinit var userRepository: UserRepository - private val user: MutableLiveData by lazy { + private val _user: MutableLiveData by lazy { loadUserFromLocal() MutableLiveData() } + val user: LiveData by lazy { + _user + } init { if (initializeComponent) { @@ -38,13 +42,13 @@ abstract class BaseViewModel(initializeComponent: Boolean = true) : ViewModel() internal val disposable = CompositeDisposable() - fun getUserData(): LiveData = user - private fun loadUserFromLocal() { - disposable.add(userRepository.getUser().observeOn(AndroidSchedulers.mainThread()).subscribe({ user.value = it }, RxErrorHandler.handleEmptyError())) + disposable.add(userRepository.getUser().observeOn(AndroidSchedulers.mainThread()) + .subscribe({ _user.value = it }, RxErrorHandler.handleEmptyError())) } fun updateUser(path: String, value: Any) { - disposable.add(userRepository.updateUser(path, value).subscribe({ }, RxErrorHandler.handleEmptyError())) + disposable.add(userRepository.updateUser(path, value) + .subscribe({ }, RxErrorHandler.handleEmptyError())) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt index 37cbd2372..1696cd616 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt @@ -61,7 +61,7 @@ open class NotificationsViewModel : BaseViewModel() { var notifications = convertInvitationsToNotifications(it) if (it.flags?.newStuff == true) { val notification = Notification() - notification.id = "new-stuff-notification" + notification.id = "custom-new-stuff-notification" notification.type = Notification.Type.NEW_STUFF.type val data = NewStuffData() notification.data = data @@ -179,11 +179,19 @@ open class NotificationsViewModel : BaseViewModel() { * instead of one of the ones coming from server. */ private fun isCustomNotification(notification: Notification): Boolean { - return notification.id.startsWith("custom-") || notification.id == "new-stuff-notification" + return notification.id.startsWith("custom-") } + private fun isCustomNewStuffNotification(notification: Notification) = + notification.id == "custom-new-stuff-notification" + fun dismissNotification(notification: Notification) { if (isCustomNotification(notification)) { + if (isCustomNewStuffNotification(notification)) { + customNotifications.onNext( + customNotifications.value?.filterNot { it.id == notification.id } + ) + } return } @@ -199,6 +207,13 @@ open class NotificationsViewModel : BaseViewModel() { .filter { !actionableNotificationTypes.contains(it.type) } .map { it.id } + val customNewStuffNotification = notifications + .firstOrNull { isCustomNewStuffNotification(it) } + + if (customNewStuffNotification != null) { + dismissNotification(customNewStuffNotification) + } + if (dismissableIds.isEmpty()) { return } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/PartyViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/PartyViewModel.kt index effce0176..03d59dc03 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/PartyViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/PartyViewModel.kt @@ -20,7 +20,7 @@ class PartyViewModel(initializeComponent: Boolean) : GroupViewModel(initializeCo internal val isUserOnQuest: Boolean get() = !( - getGroupData().value?.quest?.members?.none { it.key == getUserData().value?.id } + getGroupData().value?.quest?.members?.none { it.key == user.value?.id } ?: true ) @@ -88,7 +88,7 @@ class PartyViewModel(initializeComponent: Boolean) : GroupViewModel(initializeCo } fun showParticipantButtons(): Boolean { - val user = getUserData().value + val user = user.value return !(user?.party == null || user.party?.quest == null) && !isQuestActive && user.party?.quest?.RSVPNeeded == true } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/inventory/equipment/EquipmentOverviewViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/inventory/equipment/EquipmentOverviewViewModel.kt new file mode 100644 index 000000000..ebc740ec1 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/inventory/equipment/EquipmentOverviewViewModel.kt @@ -0,0 +1,25 @@ +package com.habitrpg.android.habitica.ui.viewmodels.inventory.equipment + +import androidx.lifecycle.SavedStateHandle +import com.habitrpg.android.habitica.components.UserComponent +import com.habitrpg.android.habitica.data.InventoryRepository +import com.habitrpg.android.habitica.helpers.RxErrorHandler +import com.habitrpg.android.habitica.models.inventory.Equipment +import com.habitrpg.android.habitica.ui.viewmodels.BaseViewModel +import javax.inject.Inject + +class EquipmentOverviewViewModel(savedStateHandle: SavedStateHandle): BaseViewModel() { + + @Inject + lateinit var inventoryRepository: InventoryRepository + + override fun inject(component: UserComponent) { + component.inject(this) + } + + fun getGear(key: String, onSuccess: (Equipment) -> Unit) { + disposable.add(inventoryRepository.getEquipment(key).subscribe( { + onSuccess(it) + }, RxErrorHandler.handleEmptyError())) + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt index 4ae3dba98..542349857 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaAlertDialog.kt @@ -312,12 +312,12 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style. private fun checkIfQueueAvailable(): Boolean { val currentDialog = dialogQueue.firstOrNull() ?: return true - if (currentDialog.isShowing) { - return false + return if (currentDialog.isShowing && currentDialog.getActivity()?.isFinishing != true) { + false } else { // The Dialog was probably dismissed in a weird way. Clear it out so that the queue doesn't get stuck dialogQueue.removeAt(0) - return true + true } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/QuestMenuView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/QuestMenuView.kt index 4e0f0bfe9..3e80cb578 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/QuestMenuView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/QuestMenuView.kt @@ -36,6 +36,7 @@ class QuestMenuView : LinearLayout { orientation = VERTICAL binding.heartIconView.setImageBitmap(HabiticaIconsHelper.imageOfHeartDarkBg()) + binding.rageIconView.setImageBitmap(HabiticaIconsHelper.imageOfRage()) binding.pendingDamageIconView.setImageBitmap(HabiticaIconsHelper.imageOfDamage()) @@ -50,16 +51,21 @@ class QuestMenuView : LinearLayout { fun configure(quest: Quest) { binding.healthBarView.setCurrentValue(quest.progress?.hp ?: 0.0) + binding.rageBarView.setCurrentValue(quest.progress?.rage ?: 0.0) } fun configure(questContent: QuestContent) { this.questContent = questContent binding.healthBarView.setMaxValue(questContent.boss?.hp?.toDouble() ?: 0.0) - binding.bottomView.setBackgroundColor(questContent.colors?.darkColor ?: 0) - //binding.bossArtView.setBackgroundColor(questContent.colors?.mediumColor ?: 0) - //DataBindingUtils.loadImage(binding.bossArtView, "quest_" + questContent.key) binding.bossNameView.text = questContent.boss?.name binding.typeTextView.text = context.getString(R.string.boss_quest) + + if (questContent.boss?.hasRage == true) { + binding.rageView.visibility = View.VISIBLE + binding.rageBarView.setMaxValue(questContent.boss?.rage?.value ?: 0.0) + } else { + binding.rageView.visibility = View.GONE + } } fun configure(user: User) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/TaskFilterDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/TaskFilterDialog.kt index 5cb70d0a9..bc02379a1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/TaskFilterDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/TaskFilterDialog.kt @@ -224,6 +224,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl if (editedTags.containsKey(tag.id)) { editedTags.remove(tag.id) } + activeTags.remove(tag.id) tags.remove(tag) tagsList.removeView(wrapper) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt index 2b46746e8..f2af43075 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt @@ -55,6 +55,12 @@ class ReminderContainer @JvmOverloads constructor( } var firstDayOfWeek: Int? = null + set(value) { + children + .filterIsInstance() + .forEach { it.firstDayOfWeek = value } + field = value + } init { orientation = VERTICAL diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index a0fb0b394..d06852bc4 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1 +1 @@ -We’ve improved more of our notifications so they bring you to the relevant screen when tapped. When customizing your avatar, you’ll be able to see your avatar updating in real time as you select different options. You can also see your current Gems when gifting Gems from your balance now. We’ve fixed a few bugs too, including one where tasks wouldn’t load when the app is left running in the background, the stats widget works again, and another where Dailies wouldn’t filter properly on launch. +In this update we’ve fixed some bugs, added more seasonal event support, and made a few quality of life improvements! Sending Guild and Party invites through email should work more reliably. We’ve improved task drag and drop logic when filters are applied to make the order more consistent. Habit streak is now referred to as ‘Habit Counter’ to better reflect the actual behavior. Issues with subscriptions not cancelling fully or getting errors when buying multiple Gem packages should be resolved.