Merge branch 'develop' of github.com:HabitRPG/habitica-android into develop

This commit is contained in:
Weblate 2021-11-02 11:26:02 +01:00
commit 4790a8dccb
53 changed files with 492 additions and 245 deletions

View file

@ -47,8 +47,8 @@ dependencies {
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
//Dependency Injection //Dependency Injection
implementation 'com.google.dagger:dagger:2.38' implementation 'com.google.dagger:dagger:2.39.1'
kapt 'com.google.dagger:dagger-compiler:2.38' kapt 'com.google.dagger:dagger-compiler:2.39.1'
compileOnly 'javax.annotation:javax.annotation-api:1.3.2' compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
compileOnly 'com.github.pengrad:jdk9-deps:1.0' compileOnly 'com.github.pengrad:jdk9-deps:1.0'
//App Compatibility and Material Design //App Compatibility and Material Design
@ -82,8 +82,8 @@ dependencies {
//Analytics //Analytics
implementation 'com.amplitude:android-sdk:2.30.0' implementation 'com.amplitude:android-sdk:2.30.0'
// Image Management Library // Image Management Library
implementation("io.coil-kt:coil:1.2.2") implementation("io.coil-kt:coil:1.4.0")
implementation("io.coil-kt:coil-gif:1.2.2") implementation("io.coil-kt:coil-gif:1.4.0")
//Tests //Tests
testImplementation 'io.kotest:kotest-runner-junit5:4.6.2' testImplementation 'io.kotest:kotest-runner-junit5:4.6.2'
@ -116,10 +116,12 @@ dependencies {
implementation 'com.nex3z:flow-layout:1.2.2' implementation 'com.nex3z:flow-layout:1.2.2'
implementation 'androidx.core:core-ktx:1.6.0' 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.lifecycle:lifecycle-common-java8:2.3.1"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-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 "androidx.paging:paging-runtime-ktx:3.0.1"
implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2' implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
@ -153,8 +155,8 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\"" 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" 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 versionCode 3070
versionName "3.4" versionName "3.4.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View file

@ -36,9 +36,8 @@
android:textSize="12sp"/> android:textSize="12sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/bottomView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="30dp" android:layout_height="28dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
tools:background="?colorPrimaryDark" tools:background="?colorPrimaryDark"
@ -73,4 +72,28 @@
tools:text="+1" tools:text="+1"
/> />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/rage_view"
android:layout_width="match_parent"
android:layout_height="28dp"
android:orientation="horizontal"
android:gravity="center_vertical"
tools:background="?colorPrimaryDark"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_large">
<ImageView
android:id="@+id/rageIconView"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="@dimen/spacing_small"/>
<com.habitrpg.android.habitica.ui.views.HabiticaProgressBar
android:id="@+id/rageBarView"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_weight="1"
android:paddingTop="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_small"
app:barForegroundColor="@color/yellow_50"
app:barBackgroundColor="@color/content_15_alpha"/>
</LinearLayout>
</merge> </merge>

View file

@ -1098,5 +1098,5 @@
<string name="spooky_promo_info_prompt">The Gem Sale is back to haunt the very end of this years Fall Gala! This is one last chance to get more Gems than ever, so stock up while it lasts!</string> <string name="spooky_promo_info_prompt">The Gem Sale is back to haunt the very end of this years Fall Gala! This is one last chance to get more Gems than ever, so stock up while it lasts!</string>
<string name="spooky_promo_info_instructions">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!</string> <string name="spooky_promo_info_instructions">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!</string>
<string name="view_gem_bundles">View Gem Bundles</string> <string name="view_gem_bundles">View Gem Bundles</string>
<string name="fall_promo_info_prompt">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.</string> <string name="fall_promo_info_prompt">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.</string>
</resources> </resources>

View file

@ -1114,7 +1114,7 @@
<string name="how_it_works">How it works</string> <string name="how_it_works">How it works</string>
<string name="limitations">Limitations</string> <string name="limitations">Limitations</string>
<string name="fall_promo_info_instructions">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!</string> <string name="fall_promo_info_instructions">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!</string>
<string name="fall_promo_info_prompt">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.</string> <string name="fall_promo_info_prompt">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.</string>
<string name="view_gem_bundles">View Gem Bundles</string> <string name="view_gem_bundles">View Gem Bundles</string>
<string name="spooky_promo_info_instructions">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!</string> <string name="spooky_promo_info_instructions">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!</string>
<string name="gems_promo_info_limitations">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.</string> <string name="gems_promo_info_limitations">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.</string>
@ -1183,4 +1183,5 @@
<string name="terms_of_service">Terms of Service</string> <string name="terms_of_service">Terms of Service</string>
<string name="damage_pending">%.01f dmg pending</string> <string name="damage_pending">%.01f dmg pending</string>
<string name="x_remaining">%s remaining</string> <string name="x_remaining">%s remaining</string>
<string name="sale_ends_in">Sale ends in %s</string>
</resources> </resources>

View file

@ -6,9 +6,11 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.database.DatabaseErrorHandler import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatDelegate 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.AppComponent
import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ApiClient 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.RxErrorHandler
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import com.habitrpg.android.habitica.modules.UserModule 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.Cache
import org.solovyev.android.checkout.Checkout import org.solovyev.android.checkout.Checkout
import org.solovyev.android.checkout.PurchaseVerifier import org.solovyev.android.checkout.PurchaseVerifier
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
// contains all HabiticaApplicationLogic except dagger componentInitialisation // contains all HabiticaApplicationLogic except dagger componentInitialisation
@ -78,6 +82,7 @@ abstract class HabiticaBaseApplication : Application() {
super.onCreate() super.onCreate()
setupRealm() setupRealm()
setupDagger() setupDagger()
setLocale()
setupRemoteConfig() setupRemoteConfig()
setupNotifications() setupNotifications()
createBillingAndCheckout() createBillingAndCheckout()
@ -117,6 +122,21 @@ abstract class HabiticaBaseApplication : Application() {
checkIfNewVersion() 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() { protected open fun setupRealm() {
Realm.init(this) Realm.init(this)
val builder = RealmConfiguration.Builder() val builder = RealmConfiguration.Builder()

View file

@ -2,6 +2,7 @@ package com.habitrpg.android.habitica
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
@ -26,32 +27,35 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
private val context: Context private val context: Context
override fun doVerify(purchases: List<Purchase>, requestListener: RequestListener<List<Purchase>>) { override fun doVerify(purchases: List<Purchase>, requestListener: RequestListener<List<Purchase>>) {
val verifiedPurchases: MutableList<Purchase> = ArrayList(purchases.size) val verifiedPurchases: MutableList<Purchase> = ArrayList(purchases.size)
val allPurchases = purchases.toMutableList()
for (purchase in purchases) { for (purchase in purchases) {
if (purchasedOrderList.contains(purchase.orderId)) { if (purchasedOrderList.contains(purchase.orderId)) {
verifiedPurchases.add(purchase) verifiedPurchases.add(purchase)
requestListener.onSuccess(verifiedPurchases) processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
} else { } else {
when { when {
PurchaseTypes.allGemTypes.contains(purchase.sku) -> { PurchaseTypes.allGemTypes.contains(purchase.sku) -> {
val validationRequest = buildValidationRequest(purchase) val validationRequest = buildValidationRequest(purchase)
apiClient.validatePurchase(validationRequest).subscribe({ apiClient.validatePurchase(validationRequest).subscribe({
purchasedOrderList.add(purchase.orderId) purchasedOrderList.add(purchase.orderId)
requestListener.onSuccess(verifiedPurchases) verifiedPurchases.add(purchase)
processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
val giftedID = removeGift(purchase.sku) val giftedID = removeGift(purchase.sku)
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID)) EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID))
}) { throwable: Throwable -> }) { throwable: Throwable ->
handleError(throwable, purchase, requestListener, verifiedPurchases) handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases)
} }
} }
PurchaseTypes.allSubscriptionNoRenewTypes.contains(purchase.sku) -> { PurchaseTypes.allSubscriptionNoRenewTypes.contains(purchase.sku) -> {
val validationRequest = buildValidationRequest(purchase) val validationRequest = buildValidationRequest(purchase)
apiClient.validateNoRenewSubscription(validationRequest).subscribe({ apiClient.validateNoRenewSubscription(validationRequest).subscribe({
purchasedOrderList.add(purchase.orderId) purchasedOrderList.add(purchase.orderId)
requestListener.onSuccess(verifiedPurchases) verifiedPurchases.add(purchase)
processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
val giftedID = removeGift(purchase.sku) val giftedID = removeGift(purchase.sku)
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID)) EventBus.getDefault().post(ConsumablePurchasedEvent(purchase, giftedID))
}) { throwable: Throwable -> }) { throwable: Throwable ->
handleError(throwable, purchase, requestListener, verifiedPurchases) handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases)
} }
} }
PurchaseTypes.allSubscriptionTypes.contains(purchase.sku) -> { PurchaseTypes.allSubscriptionTypes.contains(purchase.sku) -> {
@ -63,20 +67,35 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
apiClient.validateSubscription(validationRequest).subscribe({ apiClient.validateSubscription(validationRequest).subscribe({
purchasedOrderList.add(purchase.orderId) purchasedOrderList.add(purchase.orderId)
verifiedPurchases.add(purchase) verifiedPurchases.add(purchase)
requestListener.onSuccess(verifiedPurchases) processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
FirebaseAnalytics.getInstance(context).logEvent("user_subscribed", null) FirebaseAnalytics.getInstance(context).logEvent("user_subscribed", null)
EventBus.getDefault().post(UserSubscribedEvent()) EventBus.getDefault().post(UserSubscribedEvent())
}) { throwable: Throwable -> }) { throwable: Throwable ->
handleError(throwable, purchase, requestListener, verifiedPurchases) handleError(throwable, purchase, allPurchases, requestListener, verifiedPurchases)
} }
} }
} }
} }
} }
val edit = preferences?.edit() preferences?.edit {
edit?.putStringSet(PURCHASED_PRODUCTS_KEY, purchasedOrderList) putStringSet(PURCHASED_PRODUCTS_KEY, purchasedOrderList)
edit?.apply() }
savePendingGifts() }
private fun processedPurchase(
purchase: Purchase,
allPurchases: MutableList<Purchase>,
verifiedPurchases: MutableList<Purchase>,
requestListener: RequestListener<List<Purchase>>
) {
allPurchases.remove(purchase)
if (allPurchases.isEmpty()) {
if (verifiedPurchases.isEmpty()) {
requestListener.onError(ResponseCodes.ERROR, Exception())
} else {
requestListener.onSuccess(verifiedPurchases)
}
}
} }
private fun buildValidationRequest(purchase: Purchase): PurchaseValidationRequest { private fun buildValidationRequest(purchase: Purchase): PurchaseValidationRequest {
@ -92,13 +111,17 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
return validationRequest return validationRequest
} }
private fun handleError(throwable: Throwable, purchase: Purchase, requestListener: RequestListener<List<Purchase>>, verifiedPurchases: MutableList<Purchase>) { private fun handleError(throwable: Throwable, purchase: Purchase,
allPurchases: MutableList<Purchase>,
requestListener: RequestListener<List<Purchase>>,
verifiedPurchases: MutableList<Purchase>) {
(throwable as? HttpException)?.let { error -> (throwable as? HttpException)?.let { error ->
if (error.code() == 401) { if (error.code() == 401) {
val res = apiClient.getErrorResponse(throwable) val res = apiClient.getErrorResponse(throwable)
if (res.message != null && res.message == "RECEIPT_ALREADY_USED") { if (res.message != null && res.message == "RECEIPT_ALREADY_USED") {
purchasedOrderList.add(purchase.orderId) purchasedOrderList.add(purchase.orderId)
requestListener.onSuccess(verifiedPurchases) verifiedPurchases.add(purchase)
processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
EventBus.getDefault().post(ConsumablePurchasedEvent(purchase)) EventBus.getDefault().post(ConsumablePurchasedEvent(purchase))
removeGift(purchase.sku) removeGift(purchase.sku)
return return
@ -106,7 +129,7 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
} }
} }
FirebaseCrashlytics.getInstance().recordException(throwable) FirebaseCrashlytics.getInstance().recordException(throwable)
requestListener.onError(ResponseCodes.ERROR, Exception()) processedPurchase(purchase, allPurchases, verifiedPurchases, requestListener)
} }
private fun loadPendingGifts(): MutableMap<String?, String?> { private fun loadPendingGifts(): MutableMap<String?, String?> {
@ -131,6 +154,7 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
private const val PENDING_GIFTS_KEY = "PENDING_GIFTS" private const val PENDING_GIFTS_KEY = "PENDING_GIFTS"
private var pendingGifts: MutableMap<String?, String?> = HashMap() private var pendingGifts: MutableMap<String?, String?> = HashMap()
private var preferences: SharedPreferences? = null private var preferences: SharedPreferences? = null
fun addGift(sku: String?, userID: String?) { fun addGift(sku: String?, userID: String?) {
pendingGifts[sku] = userID pendingGifts[sku] = userID
savePendingGifts() savePendingGifts()
@ -145,10 +169,9 @@ class HabiticaPurchaseVerifier(context: Context, apiClient: ApiClient) : BasePur
private fun savePendingGifts() { private fun savePendingGifts() {
val jsonObject = JSONObject(pendingGifts as Map<*, *>) val jsonObject = JSONObject(pendingGifts as Map<*, *>)
val jsonString = jsonObject.toString() val jsonString = jsonObject.toString()
val editor = preferences?.edit() preferences?.edit {
editor?.remove(PENDING_GIFTS_KEY) putString(PENDING_GIFTS_KEY, jsonString)
editor?.putString(PENDING_GIFTS_KEY, jsonString) }
editor?.apply()
} }
} }

View file

@ -218,7 +218,7 @@ interface ApiService {
fun seenMessages(@Path("gid") groupId: String): Flowable<HabitResponse<Void>> fun seenMessages(@Path("gid") groupId: String): Flowable<HabitResponse<Void>>
@POST("groups/{gid}/invite") @POST("groups/{gid}/invite")
fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map<String, Any>): Flowable<HabitResponse<Void>> fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map<String, Any>): Flowable<HabitResponse<List<Void>>>
@POST("groups/{gid}/reject-invite") @POST("groups/{gid}/reject-invite")
fun rejectGroupInvite(@Path("gid") groupId: String): Flowable<HabitResponse<Void>> fun rejectGroupInvite(@Path("gid") groupId: String): Flowable<HabitResponse<Void>>

View file

@ -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.GroupViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModel; import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel; 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.insufficientCurrency.InsufficientGemsDialog;
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog; import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog;
import com.habitrpg.android.habitica.ui.views.social.ChatBarView; import com.habitrpg.android.habitica.ui.views.social.ChatBarView;
@ -346,4 +347,6 @@ public interface UserComponent {
void inject(@NotNull PromoWebFragment promoWebFragment); void inject(@NotNull PromoWebFragment promoWebFragment);
void inject(@NotNull ItemDialogFragment itemDialogFragment); void inject(@NotNull ItemDialogFragment itemDialogFragment);
void inject(@NotNull EquipmentOverviewViewModel equipmentOverviewViewModel);
} }

View file

@ -153,7 +153,7 @@ interface ApiClient {
fun seenMessages(groupId: String): Flowable<Void> fun seenMessages(groupId: String): Flowable<Void>
fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): Flowable<Void> fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): Flowable<List<Void>>
fun rejectGroupInvite(groupId: String): Flowable<Void> fun rejectGroupInvite(groupId: String): Flowable<Void>

View file

@ -51,7 +51,7 @@ interface SocialRepository : BaseRepository {
fun getGroupMembers(id: String): Flowable<out List<Member>> fun getGroupMembers(id: String): Flowable<out List<Member>>
fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable<List<Member>> fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable<List<Member>>
fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<Void> fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<List<Void>>
fun getMember(userId: String?): Flowable<Member> fun getMember(userId: String?): Flowable<Member>
fun getMemberWithUsername(username: String?): Flowable<Member> fun getMemberWithUsername(username: String?): Flowable<Member>
@ -60,6 +60,8 @@ interface SocialRepository : BaseRepository {
fun markPrivateMessagesRead(user: User?): Flowable<Void> fun markPrivateMessagesRead(user: User?): Flowable<Void>
fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>)
fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group> fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group>
fun removeMemberFromGroup(groupID: String, userID: String): Flowable<List<Member>> fun removeMemberFromGroup(groupID: String, userID: String): Flowable<List<Member>>

View file

@ -12,7 +12,6 @@ import com.habitrpg.android.habitica.api.GSonFactoryCreator
import com.habitrpg.android.habitica.api.HostConfig import com.habitrpg.android.habitica.api.HostConfig
import com.habitrpg.android.habitica.api.Server import com.habitrpg.android.habitica.api.Server
import com.habitrpg.android.habitica.data.ApiClient 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.events.ShowConnectionProblemEvent
import com.habitrpg.android.habitica.helpers.NotificationsManager import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.models.* import com.habitrpg.android.habitica.models.*
@ -554,7 +553,7 @@ class ApiClientImpl // private OnHabitsAPIResult mResultListener;
return apiService.seenMessages(groupId).compose(configureApiCallObserver()) return apiService.seenMessages(groupId).compose(configureApiCallObserver())
} }
override fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): Flowable<Void> { override fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): Flowable<List<Void>> {
return apiService.inviteToGroup(groupId, inviteData).compose(configureApiCallObserver()) return apiService.inviteToGroup(groupId, inviteData).compose(configureApiCallObserver())
} }

View file

@ -241,7 +241,7 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
.doOnNext { members -> localRepository.saveGroupMembers(id, members) } .doOnNext { members -> localRepository.saveGroupMembers(id, members) }
} }
override fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<Void> = apiClient.inviteToGroup(id, inviteData) override fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<List<Void>> = apiClient.inviteToGroup(id, inviteData)
override fun getMember(userId: String?): Flowable<Member> { override fun getMember(userId: String?): Flowable<Member> {
return if (userId == null) { return if (userId == null) {
@ -264,12 +264,31 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
} }
override fun markPrivateMessagesRead(user: User?): Flowable<Void> { override fun markPrivateMessagesRead(user: User?): Flowable<Void> {
if (user?.isManaged == true) {
localRepository.modify(user) {
it.inbox?.hasUserSeenInbox = true
}
}
return apiClient.markPrivateMessagesRead() return apiClient.markPrivateMessagesRead()
.doOnNext { }
if (user?.isManaged == true) {
localRepository.modify(user) { it.inbox?.newMessages = 0 } override fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>) {
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<out List<Group>> = localRepository.getUserGroups(userID, type) override fun getUserGroups(type: String?): Flowable<out List<Group>> = localRepository.getUserGroups(userID, type)

View file

@ -64,6 +64,13 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
override fun saveInboxMessages(userID: String, recipientID: String, messages: List<ChatMessage>, page: Int) { override fun saveInboxMessages(userID: String, recipientID: String, messages: List<ChatMessage>, page: Int) {
messages.forEach { it.userID = userID } 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) save(messages)
if (page != 0) return if (page != 0) return
val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).equalTo("uuid", recipientID).findAll() val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).equalTo("uuid", recipientID).findAll()

View file

@ -8,11 +8,13 @@ import io.realm.Realm
class RealmTagLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TagLocalRepository { class RealmTagLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TagLocalRepository {
override fun deleteTag(tagID: String) { override fun deleteTag(tagID: String) {
val tag = realm.where(Tag::class.java).equalTo("id", tagID).findFirst() val tags = realm.where(Tag::class.java).equalTo("id", tagID).findAll()
executeTransaction { tag?.deleteFromRealm() } executeTransaction { tags.deleteAllFromRealm() }
} }
override fun getTags(userId: String): Flowable<out List<Tag>> { override fun getTags(userId: String): Flowable<out List<Tag>> {
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()
)
} }
} }

View file

@ -90,6 +90,17 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
} }
} }
executeTransaction { realm1 -> realm1.insertOrUpdate(user) } executeTransaction { realm1 -> realm1.insertOrUpdate(user) }
removeOldTags(user.id ?: "", user.tags)
}
private fun removeOldTags(userId: String, onlineTags: List<Tag>) {
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<ChatMessage>) { override fun saveMessages(messages: List<ChatMessage>) {

View file

@ -126,6 +126,9 @@ class AppConfigManager(contentRepository: ContentRepository?) {
if (promo == null && remoteConfig.getString("activePromo").isNotBlank()) { if (promo == null && remoteConfig.getString("activePromo").isNotBlank()) {
promo = getHabiticaPromotionFromKey(remoteConfig.getString("activePromo"), null, null) promo = getHabiticaPromotionFromKey(remoteConfig.getString("activePromo"), null, null)
} }
if (promo?.isActive != true) {
return null
}
if (promo is HabiticaWebPromotion) { if (promo is HabiticaWebPromotion) {
promo.url = surveyURL() promo.url = surveyURL()
} }

View file

@ -4,6 +4,8 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.proxy.AnalyticsManager import com.habitrpg.android.habitica.proxy.AnalyticsManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.solovyev.android.checkout.* import org.solovyev.android.checkout.*
import java.util.* import java.util.*
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -74,16 +76,18 @@ open class PurchaseHandler(activity: Activity, val analyticsManager: AnalyticsMa
return purchases.skus.firstOrNull() return purchases.skus.firstOrNull()
} }
private suspend fun loadInventory(type: String, skus: List<String>): Inventory.Products? = suspendCoroutine { cont -> private suspend fun loadInventory(type: String, skus: List<String>): Inventory.Products? = withContext(Dispatchers.Main) {
val request = Inventory.Request.create().loadAllPurchases().loadSkus(type, skus) suspendCoroutine { cont ->
try { val request = Inventory.Request.create().loadAllPurchases().loadSkus(type, skus)
inventory?.load(request) { try {
cont.resume(it) inventory?.load(request) {
cont.resume(it)
}
} catch (e: NullPointerException) {
cont.resumeWithException(e)
} }
} catch (e: NullPointerException) { if (inventory == null) cont.resume(null)
cont.resumeWithException(e)
} }
if (inventory == null) cont.resume(null)
} }
fun purchaseSubscription(sku: Sku, onSuccess: (() -> Unit)) { fun purchaseSubscription(sku: Sku, onSuccess: (() -> Unit)) {

View file

@ -1,7 +1,6 @@
package com.habitrpg.android.habitica.helpers package com.habitrpg.android.habitica.helpers
import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.HabiticaBaseApplication
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import javax.inject.Inject import javax.inject.Inject
@ -17,9 +16,10 @@ class SoundManager {
HabiticaBaseApplication.userComponent?.inject(this) HabiticaBaseApplication.userComponent?.inject(this)
} }
fun preloadAllFiles(): Maybe<List<SoundFile>> { fun preloadAllFiles() {
loadedSoundFiles.clear()
if (soundTheme == SoundThemeOff) { if (soundTheme == SoundThemeOff) {
return Maybe.empty() return
} }
val soundFiles = ArrayList<SoundFile>() val soundFiles = ArrayList<SoundFile>()
@ -33,7 +33,8 @@ class SoundManager {
soundFiles.add(SoundFile(soundTheme, SoundPlusHabit)) soundFiles.add(SoundFile(soundTheme, SoundPlusHabit))
soundFiles.add(SoundFile(soundTheme, SoundReward)) soundFiles.add(SoundFile(soundTheme, SoundReward))
soundFiles.add(SoundFile(soundTheme, SoundTodo)) soundFiles.add(SoundFile(soundTheme, SoundTodo))
return soundFileLoader.download(soundFiles).toMaybe() soundFileLoader.download(soundFiles)
.subscribe({}, RxErrorHandler.handleEmptyError())
} }
fun loadAndPlayAudio(type: String) { fun loadAndPlayAudio(type: String) {

View file

@ -1,5 +1,6 @@
package com.habitrpg.android.habitica.helpers.notifications package com.habitrpg.android.habitica.helpers.notifications
import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
@ -40,6 +41,7 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
oldMessages.add(data) oldMessages.add(data)
return super.configureNotificationBuilder(data) return super.configureNotificationBuilder(data)
.setStyle(style) .setStyle(style)
.setCategory(Notification.CATEGORY_MESSAGE)
.setExtras(bundleOf(Pair("messages", bundleOf(Pair("messages", oldMessages))))) .setExtras(bundleOf(Pair("messages", bundleOf(Pair("messages", oldMessages)))))
} }
@ -55,8 +57,8 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
) )
} }
override fun setNotificationActions(data: Map<String, String>) { override fun setNotificationActions(notificationId: Int, data: Map<String, String>) {
super.setNotificationActions(data) super.setNotificationActions(notificationId, data)
val groupID = data["groupID"] ?: return val groupID = data["groupID"] ?: return
val actionName = context.getString(R.string.group_message_reply) 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) val intent = Intent(context, LocalNotificationActionReceiver::class.java)
intent.action = actionName intent.action = actionName
intent.putExtra("groupID", groupID) intent.putExtra("groupID", groupID)
intent.putExtra("NOTIFICATION_ID", notificationId)
val replyPendingIntent: PendingIntent = val replyPendingIntent: PendingIntent =
PendingIntent.getBroadcast( PendingIntent.getBroadcast(
context, groupID.hashCode(), context, groupID.hashCode(),

View file

@ -16,16 +16,18 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi
intent.putExtra("groupID", data?.get("groupID")) intent.putExtra("groupID", data?.get("groupID"))
} }
override fun setNotificationActions(data: Map<String, String>) { override fun setNotificationActions(notificationId: Int, data: Map<String, String>) {
super.setNotificationActions(data) super.setNotificationActions(notificationId, data)
val res = context.resources val res = context.resources
val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
acceptInviteIntent.action = res.getString(R.string.accept_guild_invite) 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( val pendingIntentAccept = PendingIntent.getBroadcast(
context, context,
3000, groupID.hashCode(),
acceptInviteIntent, acceptInviteIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
@ -33,10 +35,11 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi
val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
rejectInviteIntent.action = res.getString(R.string.reject_guild_invite) 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( val pendingIntentReject = PendingIntent.getBroadcast(
context, context,
2000, groupID.hashCode() + 1,
rejectInviteIntent, rejectInviteIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )

View file

@ -44,19 +44,21 @@ abstract class HabiticaLocalNotification(protected var context: Context, protect
notificationBuilder = notificationBuilder.setContentText(message) notificationBuilder = notificationBuilder.setContentText(message)
} }
this.setNotificationActions(data) val notificationId = getNotificationID(data)
this.setNotificationActions(notificationId, data)
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
notificationManager.notify(getNotificationID(data), notificationBuilder.build()) notificationManager.notify(notificationId, notificationBuilder.build())
} }
fun setExtras(data: Map<String, String>) { fun setExtras(data: Map<String, String>) {
this.data = data this.data = data
} }
protected open fun setNotificationActions(data: Map<String, String>) { protected open fun setNotificationActions(notificationId: Int, data: Map<String, String>) {
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
configureMainIntent(intent) configureMainIntent(intent)
intent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
context, context,
3000, 3000,

View file

@ -11,16 +11,18 @@ import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver
*/ */
class PartyInviteLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { class PartyInviteLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) {
override fun setNotificationActions(data: Map<String, String>) { override fun setNotificationActions(notificationId: Int, data: Map<String, String>) {
super.setNotificationActions(data) super.setNotificationActions(notificationId, data)
val res = context.resources val res = context.resources
val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
acceptInviteIntent.action = res.getString(R.string.accept_party_invite) 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( val pendingIntentAccept = PendingIntent.getBroadcast(
context, context,
3000, groupID.hashCode(),
acceptInviteIntent, acceptInviteIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
@ -28,10 +30,11 @@ class PartyInviteLocalNotification(context: Context, identifier: String?) : Habi
val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
rejectInviteIntent.action = res.getString(R.string.reject_party_invite) 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( val pendingIntentReject = PendingIntent.getBroadcast(
context, context,
2000, groupID.hashCode() + 1,
rejectInviteIntent, rejectInviteIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )

View file

@ -15,15 +15,16 @@ class QuestInviteLocalNotification(context: Context, identifier: String?) : Habi
return 1000 return 1000
} }
override fun setNotificationActions(data: Map<String, String>) { override fun setNotificationActions(notificationId: Int, data: Map<String, String>) {
super.setNotificationActions(data) super.setNotificationActions(notificationId, data)
val res = context.resources val res = context.resources
val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
acceptInviteIntent.action = res.getString(R.string.accept_quest_invite) acceptInviteIntent.action = res.getString(R.string.accept_quest_invite)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentAccept = PendingIntent.getBroadcast( val pendingIntentAccept = PendingIntent.getBroadcast(
context, context,
3000, 3001,
acceptInviteIntent, acceptInviteIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
@ -31,9 +32,10 @@ class QuestInviteLocalNotification(context: Context, identifier: String?) : Habi
val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
rejectInviteIntent.action = res.getString(R.string.reject_quest_invite) rejectInviteIntent.action = res.getString(R.string.reject_quest_invite)
rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentReject = PendingIntent.getBroadcast( val pendingIntentReject = PendingIntent.getBroadcast(
context, context,
2000, 2001,
rejectInviteIntent, rejectInviteIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )

View file

@ -1,5 +1,6 @@
package com.habitrpg.android.habitica.helpers.notifications package com.habitrpg.android.habitica.helpers.notifications
import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
@ -36,6 +37,7 @@ class ReceivedPrivateMessageLocalNotification(context: Context, identifier: Stri
} }
notification = notification notification = notification
.setContentTitle(notificationTitle) .setContentTitle(notificationTitle)
.setCategory(Notification.CATEGORY_MESSAGE)
.setStyle(style) .setStyle(style)
title = null title = null
} else { } else {
@ -48,8 +50,8 @@ class ReceivedPrivateMessageLocalNotification(context: Context, identifier: Stri
return data["senderName"].hashCode() return data["senderName"].hashCode()
} }
override fun setNotificationActions(data: Map<String, String>) { override fun setNotificationActions(notificationId: Int, data: Map<String, String>) {
super.setNotificationActions(data) super.setNotificationActions(notificationId, data)
val senderID = data["replyTo"] ?: return val senderID = data["replyTo"] ?: return
val actionName = context.getString(R.string.inbox_message_reply) 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) val intent = Intent(context, LocalNotificationActionReceiver::class.java)
intent.action = actionName intent.action = actionName
intent.putExtra("senderID", senderID) intent.putExtra("senderID", senderID)
intent.putExtra("NOTIFICATION_ID", notificationId)
val replyPendingIntent: PendingIntent = val replyPendingIntent: PendingIntent =
PendingIntent.getBroadcast( PendingIntent.getBroadcast(
context, senderID.hashCode(), context, senderID.hashCode(),

View file

@ -109,28 +109,28 @@ constructor(
return textView return textView
} }
fun getNotificationAndAddStatsToUserAsText(xp: Double, hp: Double, gold: Double, mp: Double): Pair<SpannableStringBuilder, SnackbarDisplayType> { fun getNotificationAndAddStatsToUserAsText(xp: Double?, hp: Double?, gold: Double?, mp: Double?): Pair<SpannableStringBuilder, SnackbarDisplayType> {
val builder = SpannableStringBuilder() val builder = SpannableStringBuilder()
var displayType = SnackbarDisplayType.NORMAL var displayType = SnackbarDisplayType.NORMAL
if (xp > 0) { if ((xp ?: 0.0) > 0) {
builder.append(" + ").append(xp.round(2).toString()).append(" Exp") builder.append(" + ").append(xp?.round(2).toString()).append(" Exp")
} }
if (hp != 0.0) { if (hp != 0.0) {
displayType = SnackbarDisplayType.FAILURE 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.0) {
if (gold > 0) { if ((gold ?: 0.0) > 0) {
builder.append(" + ").append(gold.round(2).toString()) builder.append(" + ").append(gold?.round(2).toString())
} else if (gold < 0) { } else if ((gold ?: 0.0) < 0) {
displayType = SnackbarDisplayType.FAILURE displayType = SnackbarDisplayType.FAILURE
builder.append(" - ").append(abs(gold.round(2)).toString()) builder.append(" - ").append(abs(gold?.round(2) ?: 0.0).toString())
} }
builder.append(" Gold") builder.append(" Gold")
} }
if (mp > 0) { if ((mp ?: 0.0) > 0) {
builder.append(" + ").append(mp.round(2).toString()).append(" Mana") builder.append(" + ").append(mp?.round(2).toString()).append(" Mana")
} }
return Pair(builder, displayType) return Pair(builder, displayType)

View file

@ -17,6 +17,11 @@ enum class PromoType {
} }
abstract class HabiticaPromotion { abstract class HabiticaPromotion {
val isActive: Boolean
get() {
val now = Date()
return startDate.before(now) && endDate.after(now)
}
abstract val identifier: String abstract val identifier: String
abstract val promoType: PromoType abstract val promoType: PromoType

View file

@ -58,6 +58,8 @@ open class ChatMessage : RealmObject(), BaseMainObject {
val formattedUsername: String? val formattedUsername: String?
get() = if (username != null) "@$username" else null get() = if (username != null) "@$username" else null
var isSeen: Boolean = true
fun userLikesMessage(userId: String?): Boolean { fun userLikesMessage(userId: String?): Boolean {
return likes?.any { userId == it.id } ?: false return likes?.any { userId == it.id } ?: false
} }

View file

@ -10,4 +10,5 @@ open class Inbox : RealmObject(), BaseObject {
var optOut: Boolean = false var optOut: Boolean = false
var blocks: RealmList<String> = RealmList() var blocks: RealmList<String> = RealmList()
var newMessages: Int = 0 var newMessages: Int = 0
var hasUserSeenInbox: Boolean = false
} }

View file

@ -4,6 +4,9 @@ import android.app.NotificationManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent 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.NotificationManagerCompat
import androidx.core.app.RemoteInput import androidx.core.app.RemoteInput
import com.habitrpg.android.habitica.HabiticaBaseApplication 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.TaskRepository
import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.interactors.NotifyUserUseCase
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import javax.inject.Inject import javax.inject.Inject
class LocalNotificationActionReceiver : BroadcastReceiver() { class LocalNotificationActionReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var userRepository: UserRepository lateinit var userRepository: UserRepository
@Inject @Inject
lateinit var socialRepository: SocialRepository lateinit var socialRepository: SocialRepository
@Inject @Inject
lateinit var taskRepository: TaskRepository lateinit var taskRepository: TaskRepository
@Inject @Inject
lateinit var apiClient: ApiClient lateinit var apiClient: ApiClient
private var user: User? = null private var user: User? = null
private var groupID: String? = null private val groupID: String?
private var senderID: String? = null 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 context: Context? = null
private var intent: Intent? = null private var intent: Intent? = null
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
HabiticaBaseApplication.userComponent?.inject(this) HabiticaBaseApplication.userComponent?.inject(this)
this.intent = intent this.intent = intent
groupID = intent.extras?.getString("groupID")
senderID = intent.extras?.getString("senderID")
this.context = context this.context = context
handleLocalNotificationAction(intent.action) handleLocalNotificationAction(intent.action)
} }
private fun handleLocalNotificationAction(action: String?) { private fun handleLocalNotificationAction(action: String?) {
val notificationManager = this.context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager val notificationManager =
notificationManager?.cancelAll() this.context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.cancel(intent?.extras?.getInt("NOTIFICATION_ID") ?: -1)
when (action) { when (action) {
context?.getString(R.string.accept_party_invite) -> { context?.getString(R.string.accept_party_invite) -> {
groupID?.let { groupID?.let {
@ -52,7 +62,8 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
} }
context?.getString(R.string.reject_party_invite) -> { context?.getString(R.string.reject_party_invite) -> {
groupID?.let { groupID?.let {
socialRepository.rejectGroupInvite(it).subscribe({ }, RxErrorHandler.handleEmptyError()) socialRepository.rejectGroupInvite(it)
.subscribe({ }, RxErrorHandler.handleEmptyError())
} }
} }
context?.getString(R.string.accept_quest_invite) -> { context?.getString(R.string.accept_quest_invite) -> {
@ -68,7 +79,8 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
} }
context?.getString(R.string.reject_guild_invite) -> { context?.getString(R.string.reject_guild_invite) -> {
groupID?.let { groupID?.let {
socialRepository.rejectGroupInvite(it).subscribe({ }, RxErrorHandler.handleEmptyError()) socialRepository.rejectGroupInvite(it)
.subscribe({ }, RxErrorHandler.handleEmptyError())
} }
} }
context?.getString(R.string.group_message_reply) -> { context?.getString(R.string.group_message_reply) -> {
@ -76,7 +88,9 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
getMessageText(context?.getString(R.string.group_message_reply))?.let { message -> getMessageText(context?.getString(R.string.group_message_reply))?.let { message ->
socialRepository.postGroupChat(it, message).subscribe( 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() RxErrorHandler.handleEmptyError()
) )
@ -86,19 +100,33 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
context?.getString(R.string.inbox_message_reply) -> { context?.getString(R.string.inbox_message_reply) -> {
senderID?.let { senderID?.let {
getMessageText(context?.getString(R.string.inbox_message_reply))?.let { message -> 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) -> { context?.getString(R.string.complete_task_action) -> {
intent?.extras?.getString("taskID")?.let { taskID?.let {
taskRepository.taskChecked(null, it, up = true, force = false) { 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? { private fun getMessageText(key: String?): String? {
return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(key)?.toString() return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(key)?.toString()
} }

View file

@ -1,10 +1,12 @@
package com.habitrpg.android.habitica.receivers package com.habitrpg.android.habitica.receivers
import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.HabiticaBaseApplication
@ -62,21 +64,29 @@ class TaskReceiver : BroadcastReceiver() {
val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0) val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0)
val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) 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) .setSmallIcon(R.drawable.ic_gryphon_white)
.setContentTitle(task.text) .setContentTitle(task.text)
.setStyle(NotificationCompat.BigTextStyle()
.bigText(task.notes))
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setSound(soundUri) .setSound(soundUri)
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(pendingIntent) .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) { if (task.type == Task.TYPE_DAILY || task.type == Task.TYPE_TODO) {
val completeIntent = Intent(context, LocalNotificationActionReceiver::class.java) val completeIntent = Intent(context, LocalNotificationActionReceiver::class.java).apply {
completeIntent.action = context.getString(R.string.complete_task_action) action = context.getString(R.string.complete_task_action)
completeIntent.putExtra("taskID", task.id) putExtra("taskID", task.id)
putExtra("NOTIFICATION_ID", task.id.hashCode())
}
val pendingIntentComplete = PendingIntent.getBroadcast( val pendingIntentComplete = PendingIntent.getBroadcast(
context, context,
3000, task.id.hashCode(),
completeIntent, completeIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )

View file

@ -12,6 +12,7 @@ import androidx.core.view.marginStart
import androidx.core.view.marginTop import androidx.core.view.marginTop
import coil.clear import coil.clear
import coil.load import coil.load
import coil.target.ImageViewTarget
import com.habitrpg.android.habitica.BuildConfig import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.dpToPx import com.habitrpg.android.habitica.extensions.dpToPx
@ -143,17 +144,15 @@ class AvatarView : FrameLayout {
imageView.load(DataBindingUtils.BASE_IMAGE_URL + DataBindingUtils.getFullFilename(layerName)) { imageView.load(DataBindingUtils.BASE_IMAGE_URL + DataBindingUtils.getFullFilename(layerName)) {
allowHardware(false) allowHardware(false)
target( target(object : ImageViewTarget(imageView) {
{}, override fun onError(error: Drawable?) {
{ super.onError(error)
onLayerComplete() onLayerComplete()
}, }
{
if (imageView.tag != layerName) { override fun onSuccess(result: Drawable) {
return@target super.onSuccess(result)
} val bounds = getLayerBounds(layerKey, layerName, result)
val bounds = getLayerBounds(layerKey, layerName, it)
imageView.setImageDrawable(it)
imageView.imageMatrix = avatarMatrix imageView.imageMatrix = avatarMatrix
val layoutParams = imageView.layoutParams as? LayoutParams val layoutParams = imageView.layoutParams as? LayoutParams
layoutParams?.topMargin = bounds.top layoutParams?.topMargin = bounds.top
@ -163,7 +162,7 @@ class AvatarView : FrameLayout {
imageView.layoutParams = layoutParams imageView.layoutParams = layoutParams
onLayerComplete() onLayerComplete()
} }
) })
} }
} }
while (i < (imageViewHolder.size)) { while (i < (imageViewHolder.size)) {

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@ -61,7 +62,7 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val languageHelper = LanguageHelper(sharedPreferences.getString("language", "en")) val languageHelper = LanguageHelper(sharedPreferences.getString("language", "en"))
resources.forceLocale(languageHelper.locale) resources.forceLocale(this, languageHelper.locale)
delegate.localNightMode = when (sharedPreferences.getString("theme_mode", "system")) { delegate.localNightMode = when (sharedPreferences.getString("theme_mode", "system")) {
"light" -> AppCompatDelegate.MODE_NIGHT_NO "light" -> AppCompatDelegate.MODE_NIGHT_NO
"dark" -> AppCompatDelegate.MODE_NIGHT_YES "dark" -> AppCompatDelegate.MODE_NIGHT_YES
@ -77,6 +78,13 @@ abstract class BaseActivity : AppCompatActivity() {
compositeSubscription = CompositeDisposable() 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() { override fun onStart() {
super.onStart() super.onStart()
EventBus.getDefault().register(this) 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) Locale.setDefault(locale)
val configuration = Configuration() val configuration = Configuration()
configuration.setLocale(locale) configuration.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.createConfigurationContext(configuration)
}
updateConfiguration(configuration, displayMetrics) updateConfiguration(configuration, displayMetrics)
} }

View file

@ -847,7 +847,7 @@ open class MainActivity : BaseActivity(), TutorialView.OnTutorialReaction {
} }
@Subscribe @Subscribe
fun showWonAchievementDialog(event: ShowWonChallengeDialog) { fun showWonChallengeDialog(event: ShowWonChallengeDialog) {
retrieveUser(true) retrieveUser(true)
lifecycleScope.launch(context = Dispatchers.Main) { lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = WonChallengeDialog(this@MainActivity) val dialog = WonChallengeDialog(this@MainActivity)

View file

@ -20,7 +20,7 @@ class PrefsActivity : BaseActivity(), PreferenceFragmentCompat.OnPreferenceStart
setupToolbar(findViewById(R.id.toolbar)) setupToolbar(findViewById(R.id.toolbar))
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, PreferencesFragment()) .replace(R.id.fragment_container, PreferencesFragment())
.commit() .commit()
} }

View file

@ -16,6 +16,7 @@ import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.SimpleItemAnimator
import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.ContentRepository 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.MainNavigationController
import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.WorldState 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.Item
import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion
import com.habitrpg.android.habitica.models.promotions.PromoType import com.habitrpg.android.habitica.models.promotions.PromoType
import com.habitrpg.android.habitica.models.social.Group 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.models.user.User
import com.habitrpg.android.habitica.ui.activities.MainActivity import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.activities.NotificationsActivity import com.habitrpg.android.habitica.ui.activities.NotificationsActivity
@ -98,6 +101,7 @@ class NavigationDrawerFragment : DialogFragment() {
private fun updateQuestDisplay() { private fun updateQuestDisplay() {
val quest = this.quest val quest = this.quest
val questContent = this.questContent val questContent = this.questContent
return
if (quest == null || questContent == null || !quest.active) { if (quest == null || questContent == null || !quest.active) {
binding?.questMenuView?.visibility = View.GONE binding?.questMenuView?.visibility = View.GONE
context?.let { context?.let {
@ -125,20 +129,6 @@ class NavigationDrawerFragment : DialogFragment() {
} }
binding?.questMenuView?.setBackgroundColor(context?.getThemeColor(R.attr.colorPrimaryDark) ?: 0) 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 { binding?.questMenuView?.setOnClickListener {
setSelection(R.id.partyFragment) setSelection(R.id.partyFragment)
/*val context = this.context /*val context = this.context
@ -181,6 +171,7 @@ class NavigationDrawerFragment : DialogFragment() {
binding?.recyclerView?.adapter = adapter binding?.recyclerView?.adapter = adapter
binding?.recyclerView?.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) binding?.recyclerView?.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
(binding?.recyclerView?.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
initializeMenuItems() initializeMenuItems()
subscriptions?.add( subscriptions?.add(
@ -237,12 +228,12 @@ class NavigationDrawerFragment : DialogFragment() {
{ pair -> { pair ->
val gearEvent = pair.first.events.firstOrNull { it.gear } val gearEvent = pair.first.events.firstOrNull { it.gear }
createUpdatingJob("seasonal", { createUpdatingJob("seasonal", {
gearEvent?.end?.after(Date()) == true || pair.second.isNotEmpty() gearEvent?.isCurrentlyActive == true || pair.second.isNotEmpty()
}, { }, {
val diff = (gearEvent?.end?.time ?: 0) - Date().time val diff = (gearEvent?.end?.time ?: 0) - Date().time
if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1) if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1)
}) { }) {
updateSeasonalMenuEntries(pair.first, pair.second) updateSeasonalMenuEntries(gearEvent, pair.second)
} }
}, },
RxErrorHandler.handleEmptyError() RxErrorHandler.handleEmptyError()
@ -278,6 +269,7 @@ class NavigationDrawerFragment : DialogFragment() {
it.quest?.key ?: "" it.quest?.key ?: ""
} }
.flatMapMaybe { inventoryRepository.getQuestContent(it).firstElement() } .flatMapMaybe { inventoryRepository.getQuestContent(it).firstElement() }
.filter { (it.boss?.hp ?: 0) > 0 }
.subscribe( .subscribe(
{ {
questContent = it questContent = it
@ -310,7 +302,7 @@ class NavigationDrawerFragment : DialogFragment() {
} }
} }
private fun updateSeasonalMenuEntries(worldState: WorldState, items: List<Item>) { private fun updateSeasonalMenuEntries(gearEvent: WorldStateEvent?, items: List<Item>) {
val market = getItemWithIdentifier(SIDEBAR_SHOPS_MARKET) ?: return val market = getItemWithIdentifier(SIDEBAR_SHOPS_MARKET) ?: return
if (items.isNotEmpty() && items.firstOrNull()?.event?.end?.after(Date()) == true) { if (items.isNotEmpty() && items.firstOrNull()?.event?.end?.after(Date()) == true) {
market.pillText = context?.getString(R.string.something_new) market.pillText = context?.getString(R.string.something_new)
@ -323,8 +315,7 @@ class NavigationDrawerFragment : DialogFragment() {
val shop = getItemWithIdentifier(SIDEBAR_SHOPS_SEASONAL) ?: return val shop = getItemWithIdentifier(SIDEBAR_SHOPS_SEASONAL) ?: return
shop.pillText = context?.getString(R.string.open) shop.pillText = context?.getString(R.string.open)
val gearEvent = worldState.events.firstOrNull { it.gear } if (gearEvent?.isCurrentlyActive == true) {
if (gearEvent?.end?.after(Date()) == true) {
shop.isVisible = true shop.isVisible = true
shop.subtitle = context?.getString(R.string.open_for, gearEvent.end?.getShortRemainingString()) shop.subtitle = context?.getString(R.string.open_for, gearEvent.end?.getShortRemainingString())
} else { } else {
@ -334,7 +325,7 @@ class NavigationDrawerFragment : DialogFragment() {
} }
private fun updateUser(user: User) { private fun updateUser(user: User) {
setMessagesCount(user.inbox?.newMessages ?: 0) setMessagesCount(user.inbox)
setSettingsCount(if (user.flags?.verifiedUsername != true) 1 else 0) setSettingsCount(if (user.flags?.verifiedUsername != true) 1 else 0)
setDisplayName(user.profile?.name) setDisplayName(user.profile?.name)
setUsername(user.formattedUsername) setUsername(user.formattedUsername)
@ -629,12 +620,23 @@ class NavigationDrawerFragment : DialogFragment() {
} }
} }
private fun setMessagesCount(unreadMessages: Int) { private fun setMessagesCount(inbox: Inbox?) {
if (unreadMessages == 0) { val numOfUnreadMessages = inbox?.newMessages ?: 0
binding?.messagesBadge?.visibility = View.GONE if (numOfUnreadMessages != 0) {
} else {
binding?.messagesBadge?.visibility = View.VISIBLE 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() activePromo = configManager.activePromo()
val promoItem = getItemWithIdentifier(SIDEBAR_PROMO) ?: return val promoItem = getItemWithIdentifier(SIDEBAR_PROMO) ?: return
activePromo?.let { activePromo -> activePromo?.let { activePromo ->
if (sharedPreferences.getBoolean("hide${activePromo.identifier}", false)) { promoItem.isVisible =
promoItem.isVisible = true !sharedPreferences.getBoolean("hide${activePromo.identifier}", false)
adapter.activePromo = activePromo adapter.activePromo = activePromo
} else {
promoItem.isVisible = false
}
var promotedItem: HabiticaDrawerItem? = null var promotedItem: HabiticaDrawerItem? = null
if (activePromo.promoType == PromoType.GEMS_AMOUNT || activePromo.promoType == PromoType.GEMS_PRICE) { if (activePromo.promoType == PromoType.GEMS_AMOUNT || activePromo.promoType == PromoType.GEMS_PRICE) {
promotedItem = getItemWithIdentifier(SIDEBAR_GEMS) promotedItem = getItemWithIdentifier(SIDEBAR_GEMS)
@ -670,13 +668,18 @@ class NavigationDrawerFragment : DialogFragment() {
promotedItem.pillText = context?.getString(R.string.sale) promotedItem.pillText = context?.getString(R.string.sale)
promotedItem.pillBackground = context?.let { activePromo.pillBackgroundDrawable(it) } promotedItem.pillBackground = context?.let { activePromo.pillBackgroundDrawable(it) }
createUpdatingJob(activePromo.promoType.name, { createUpdatingJob(activePromo.promoType.name, {
activePromo.endDate.after(Date()) activePromo.isActive
}, { }, {
val diff = activePromo.endDate.time - Date().time val diff = activePromo.endDate.time - Date().time
if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1) if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1)
}) { }) {
promotedItem.subtitle = context?.getString(R.string.x_remaining, activePromo.endDate.getShortRemainingString()) if (activePromo.isActive) {
updateItem(promotedItem) promotedItem.subtitle = context?.getString(R.string.sale_ends_in, activePromo.endDate.getShortRemainingString())
updateItem(promotedItem)
} else {
promotedItem.subtitle = null
updateItem(promotedItem)
}
} }
} ?: run { } ?: run {
promoItem.isVisible = false promoItem.isVisible = false

View file

@ -33,7 +33,6 @@ class EquipmentDetailFragment :
} }
var type: String? = null var type: String? = null
var typeText: String? = null
var equippedGear: String? = null var equippedGear: String? = null
var isCostume: Boolean? = null var isCostume: Boolean? = null

View file

@ -4,38 +4,26 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.viewModels
import com.habitrpg.android.habitica.components.UserComponent 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.databinding.FragmentEquipmentOverviewBinding
import com.habitrpg.android.habitica.helpers.MainNavigationController 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.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 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<FragmentEquipmentOverviewBinding>() { class EquipmentOverviewFragment : BaseMainFragment<FragmentEquipmentOverviewBinding>() {
private val viewModel: EquipmentOverviewViewModel by viewModels()
override var binding: FragmentEquipmentOverviewBinding? = null override var binding: FragmentEquipmentOverviewBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentEquipmentOverviewBinding { override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentEquipmentOverviewBinding {
return FragmentEquipmentOverviewBinding.inflate(inflater, container, false) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -46,22 +34,24 @@ class EquipmentOverviewFragment : BaseMainFragment<FragmentEquipmentOverviewBind
displayEquipmentDetailList(type, equipped, true) displayEquipmentDetailList(type, equipped, true)
} }
binding?.autoEquipSwitch?.isChecked = user?.preferences?.autoEquip ?: false binding?.autoEquipSwitch?.setOnCheckedChangeListener { _, isChecked ->
binding?.costumeSwitch?.isChecked = user?.preferences?.costume ?: false 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()) } viewModel.user.observe(viewLifecycleOwner) {
binding?.costumeSwitch?.setOnCheckedChangeListener { _, isChecked -> userRepository.updateUser("preferences.costume", isChecked).subscribe({ }, RxErrorHandler.handleEmptyError()) } it?.items?.gear?.let {
updateGearData(it)
}
binding?.autoEquipSwitch?.isChecked = user?.preferences?.autoEquip ?: false
binding?.costumeSwitch?.isChecked = user?.preferences?.costume ?: false
user?.items?.gear?.let { binding?.costumeView?.isEnabled = user?.preferences?.costume == true
updateGearData(it)
} }
} }
override fun onDestroy() {
inventoryRepository.close()
super.onDestroy()
}
override fun injectFragment(component: UserComponent) { override fun injectFragment(component: UserComponent) {
component.inject(this) component.inject(this)
} }
@ -71,31 +61,17 @@ class EquipmentOverviewFragment : BaseMainFragment<FragmentEquipmentOverviewBind
} }
private fun updateGearData(gear: Gear) { private fun updateGearData(gear: Gear) {
if (gear.equipped?.weapon?.isNotEmpty() == true) { updateOutfit(binding?.battlegearView, gear.equipped)
compositeSubscription.add( updateOutfit(binding?.costumeView, gear.costume)
inventoryRepository.getEquipment(gear.equipped?.weapon ?: "").firstElement() }
.subscribe(
{ private fun updateOutfit(view: EquipmentOverviewView?, outfit: Outfit?) {
binding?.battlegearView?.updateData(gear.equipped, it.twoHanded) if (outfit?.weapon?.isNotEmpty() == true) {
}, viewModel.getGear(outfit.weapon) {
RxErrorHandler.handleEmptyError() view?.updateData(outfit, it.twoHanded)
) }
)
} else { } else {
binding?.battlegearView?.updateData(gear.equipped) view?.updateData(outfit)
}
if (gear.costume?.weapon?.isNotEmpty() == true) {
compositeSubscription.add(
inventoryRepository.getEquipment(gear.costume?.weapon ?: "").firstElement()
.subscribe(
{
binding?.costumeView?.updateData(gear.costume, it.twoHanded)
},
RxErrorHandler.handleEmptyError()
)
)
} else {
binding?.costumeView?.updateData(gear.costume)
} }
} }
} }

View file

@ -26,6 +26,7 @@ import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
class GemsPurchaseFragment : BaseFragment<FragmentGemPurchaseBinding>(), GemPurchaseActivity.CheckoutFragment { class GemsPurchaseFragment : BaseFragment<FragmentGemPurchaseBinding>(), GemPurchaseActivity.CheckoutFragment {
@ -98,8 +99,10 @@ class GemsPurchaseFragment : BaseFragment<FragmentGemPurchaseBinding>(), GemPurc
override fun setupCheckout() { override fun setupCheckout() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val skus = purchaseHandler?.getAllGemSKUs() val skus = purchaseHandler?.getAllGemSKUs()
for (sku in skus ?: emptyList()) { withContext(Dispatchers.Main) {
updateButtonLabel(sku.id.code, sku.price) for (sku in skus ?: emptyList()) {
updateButtonLabel(sku.id.code, sku.price)
}
} }
} }
} }

View file

@ -18,6 +18,7 @@ import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
class GiftPurchaseGemsFragment : BaseFragment<FragmentGiftGemPurchaseBinding>() { class GiftPurchaseGemsFragment : BaseFragment<FragmentGiftGemPurchaseBinding>() {
@ -63,8 +64,10 @@ class GiftPurchaseGemsFragment : BaseFragment<FragmentGiftGemPurchaseBinding>()
fun setupCheckout() { fun setupCheckout() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val skus = purchaseHandler?.getAllGemSKUs() val skus = purchaseHandler?.getAllGemSKUs()
for (sku in skus ?: emptyList()) { withContext(Dispatchers.Main) {
updateButtonLabel(sku.id.code, sku.price) for (sku in skus ?: emptyList()) {
updateButtonLabel(sku.id.code, sku.price)
}
} }
} }
} }

View file

@ -35,6 +35,7 @@ import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionVi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.solovyev.android.checkout.Inventory import org.solovyev.android.checkout.Inventory
import org.solovyev.android.checkout.Purchase import org.solovyev.android.checkout.Purchase
@ -145,12 +146,15 @@ class SubscriptionFragment : BaseFragment<FragmentSubscriptionBinding>(), GemPur
override fun setupCheckout() { override fun setupCheckout() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val subscriptions = purchaseHandler?.getAllSubscriptionProducts() ?: return@launch val subscriptions = purchaseHandler?.getAllSubscriptionProducts() ?: return@launch
for (sku in subscriptions.skus) { skus = subscriptions.skus
updateButtonLabel(sku, sku.price, subscriptions) 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()
} }
} }

View file

@ -101,7 +101,7 @@ class ChatFragment : BaseFragment<FragmentChatBinding>() {
viewModel?.updateUser("flags.communityGuidelinesAccepted", true) viewModel?.updateUser("flags.communityGuidelinesAccepted", true)
} }
viewModel?.getUserData()?.observe( viewModel?.user?.observe(
viewLifecycleOwner, viewLifecycleOwner,
{ {
chatAdapter?.user = it chatAdapter?.user = it

View file

@ -88,7 +88,12 @@ class InboxMessageListFragment : BaseMainFragment<FragmentInboxMessageListBindin
setReceivingUser(member.username, member.id) setReceivingUser(member.username, member.id)
activity?.title = member.displayName activity?.title = member.displayName
chatAdapter = InboxAdapter(user, member) chatAdapter = InboxAdapter(user, member)
viewModel?.messages?.observe(this.viewLifecycleOwner, { chatAdapter?.submitList(it) }) viewModel?.messages?.observe(
this.viewLifecycleOwner
) {
markMessagesAsRead(it)
chatAdapter?.submitList(it)
}
binding?.recyclerView?.adapter = chatAdapter binding?.recyclerView?.adapter = chatAdapter
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator() binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
@ -165,6 +170,10 @@ class InboxMessageListFragment : BaseMainFragment<FragmentInboxMessageListBindin
component.inject(this) component.inject(this)
} }
private fun markMessagesAsRead(messages: List<ChatMessage>) {
socialRepository.markSomePrivateMessagesAsRead(user, messages)
}
private fun startAutoRefreshing() { private fun startAutoRefreshing() {
if (refreshDisposable != null && refreshDisposable?.isDisposed != true) { if (refreshDisposable != null && refreshDisposable?.isDisposed != true) {
refreshDisposable?.dispose() refreshDisposable?.dispose()

View file

@ -111,21 +111,22 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
private val sendInvitesResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val sendInvitesResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) { if (it.resultCode == Activity.RESULT_OK) {
val inviteData = HashMap<String, Any>() val inviteData = HashMap<String, Any>()
inviteData["inviter"] = viewModel?.getUserData()?.value?.profile?.name ?: "" inviteData["inviter"] = viewModel?.user?.value?.profile?.name ?: ""
if (it.data?.getBooleanExtra(GroupInviteActivity.IS_EMAIL_KEY, false) == true) { val emails = it.data?.getStringArrayExtra(GroupInviteActivity.EMAILS_KEY)
val emails = it.data?.getStringArrayExtra(GroupInviteActivity.EMAILS_KEY) if (emails != null && emails.isNotEmpty()) {
val invites = ArrayList<HashMap<String, String>>() val invites = ArrayList<HashMap<String, String>>()
emails?.forEach { email -> emails.forEach { email ->
val invite = HashMap<String, String>() val invite = HashMap<String, String>()
invite["name"] = "" invite["name"] = ""
invite["email"] = email invite["email"] = email
invites.add(invite) invites.add(invite)
} }
inviteData["emails"] = invites inviteData["emails"] = invites
} else { }
val userIDs = it.data?.getStringArrayExtra(GroupInviteActivity.USER_IDS_KEY) val userIDs = it.data?.getStringArrayExtra(GroupInviteActivity.USER_IDS_KEY)
val invites = mutableListOf<String>() if (userIDs != null && userIDs.isNotEmpty()) {
userIDs?.forEach { invites.add(it) } val invites = ArrayList<String>()
userIDs.forEach { invites.add(it) }
inviteData["usernames"] = invites inviteData["usernames"] = invites
} }
viewModel?.inviteToGroup(inviteData) viewModel?.inviteToGroup(inviteData)

View file

@ -114,7 +114,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
} }
viewModel?.getGroupData()?.observe(viewLifecycleOwner, { updateParty(it) }) viewModel?.getGroupData()?.observe(viewLifecycleOwner, { updateParty(it) })
viewModel?.getUserData()?.observe(viewLifecycleOwner, { updateUser(it) }) viewModel?.user?.observe(viewLifecycleOwner, { updateUser(it) })
viewModel?.getMembersData()?.observe(viewLifecycleOwner, { updateMembersList(it) }) viewModel?.getMembersData()?.observe(viewLifecycleOwner, { updateMembersList(it) })
} }
@ -269,7 +269,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
} }
) ?: return@forEachIndexed ) ?: return@forEachIndexed
val viewHolder = GroupMemberViewHolder(memberView) val viewHolder = GroupMemberViewHolder(memberView)
viewHolder.bind(member, leaderID ?: "", viewModel?.getUserData()?.value?.id) viewHolder.bind(member, leaderID ?: "", viewModel?.user?.value?.id)
viewHolder.onClickEvent = { viewHolder.onClickEvent = {
FullProfileActivity.open(member.id ?: "") FullProfileActivity.open(member.id ?: "")
} }

View file

@ -7,6 +7,7 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -17,10 +18,13 @@ abstract class BaseViewModel(initializeComponent: Boolean = true) : ViewModel()
@Inject @Inject
lateinit var userRepository: UserRepository lateinit var userRepository: UserRepository
private val user: MutableLiveData<User?> by lazy { private val _user: MutableLiveData<User?> by lazy {
loadUserFromLocal() loadUserFromLocal()
MutableLiveData<User?>() MutableLiveData<User?>()
} }
val user: LiveData<User?> by lazy {
_user
}
init { init {
if (initializeComponent) { if (initializeComponent) {
@ -38,13 +42,13 @@ abstract class BaseViewModel(initializeComponent: Boolean = true) : ViewModel()
internal val disposable = CompositeDisposable() internal val disposable = CompositeDisposable()
fun getUserData(): LiveData<User?> = user
private fun loadUserFromLocal() { 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) { fun updateUser(path: String, value: Any) {
disposable.add(userRepository.updateUser(path, value).subscribe({ }, RxErrorHandler.handleEmptyError())) disposable.add(userRepository.updateUser(path, value)
.subscribe({ }, RxErrorHandler.handleEmptyError()))
} }
} }

View file

@ -61,7 +61,7 @@ open class NotificationsViewModel : BaseViewModel() {
var notifications = convertInvitationsToNotifications(it) var notifications = convertInvitationsToNotifications(it)
if (it.flags?.newStuff == true) { if (it.flags?.newStuff == true) {
val notification = Notification() val notification = Notification()
notification.id = "new-stuff-notification" notification.id = "custom-new-stuff-notification"
notification.type = Notification.Type.NEW_STUFF.type notification.type = Notification.Type.NEW_STUFF.type
val data = NewStuffData() val data = NewStuffData()
notification.data = data notification.data = data
@ -179,11 +179,19 @@ open class NotificationsViewModel : BaseViewModel() {
* instead of one of the ones coming from server. * instead of one of the ones coming from server.
*/ */
private fun isCustomNotification(notification: Notification): Boolean { 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) { fun dismissNotification(notification: Notification) {
if (isCustomNotification(notification)) { if (isCustomNotification(notification)) {
if (isCustomNewStuffNotification(notification)) {
customNotifications.onNext(
customNotifications.value?.filterNot { it.id == notification.id }
)
}
return return
} }
@ -199,6 +207,13 @@ open class NotificationsViewModel : BaseViewModel() {
.filter { !actionableNotificationTypes.contains(it.type) } .filter { !actionableNotificationTypes.contains(it.type) }
.map { it.id } .map { it.id }
val customNewStuffNotification = notifications
.firstOrNull { isCustomNewStuffNotification(it) }
if (customNewStuffNotification != null) {
dismissNotification(customNewStuffNotification)
}
if (dismissableIds.isEmpty()) { if (dismissableIds.isEmpty()) {
return return
} }

View file

@ -20,7 +20,7 @@ class PartyViewModel(initializeComponent: Boolean) : GroupViewModel(initializeCo
internal val isUserOnQuest: Boolean internal val isUserOnQuest: Boolean
get() = !( get() = !(
getGroupData().value?.quest?.members?.none { it.key == getUserData().value?.id } getGroupData().value?.quest?.members?.none { it.key == user.value?.id }
?: true ?: true
) )
@ -88,7 +88,7 @@ class PartyViewModel(initializeComponent: Boolean) : GroupViewModel(initializeCo
} }
fun showParticipantButtons(): Boolean { 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 return !(user?.party == null || user.party?.quest == null) && !isQuestActive && user.party?.quest?.RSVPNeeded == true
} }

View file

@ -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()))
}
}

View file

@ -312,12 +312,12 @@ open class HabiticaAlertDialog(context: Context) : AlertDialog(context, R.style.
private fun checkIfQueueAvailable(): Boolean { private fun checkIfQueueAvailable(): Boolean {
val currentDialog = dialogQueue.firstOrNull() ?: return true val currentDialog = dialogQueue.firstOrNull() ?: return true
if (currentDialog.isShowing) { return if (currentDialog.isShowing && currentDialog.getActivity()?.isFinishing != true) {
return false false
} else { } else {
// The Dialog was probably dismissed in a weird way. Clear it out so that the queue doesn't get stuck // The Dialog was probably dismissed in a weird way. Clear it out so that the queue doesn't get stuck
dialogQueue.removeAt(0) dialogQueue.removeAt(0)
return true true
} }
} }
} }

View file

@ -36,6 +36,7 @@ class QuestMenuView : LinearLayout {
orientation = VERTICAL orientation = VERTICAL
binding.heartIconView.setImageBitmap(HabiticaIconsHelper.imageOfHeartDarkBg()) binding.heartIconView.setImageBitmap(HabiticaIconsHelper.imageOfHeartDarkBg())
binding.rageIconView.setImageBitmap(HabiticaIconsHelper.imageOfRage())
binding.pendingDamageIconView.setImageBitmap(HabiticaIconsHelper.imageOfDamage()) binding.pendingDamageIconView.setImageBitmap(HabiticaIconsHelper.imageOfDamage())
@ -50,16 +51,21 @@ class QuestMenuView : LinearLayout {
fun configure(quest: Quest) { fun configure(quest: Quest) {
binding.healthBarView.setCurrentValue(quest.progress?.hp ?: 0.0) binding.healthBarView.setCurrentValue(quest.progress?.hp ?: 0.0)
binding.rageBarView.setCurrentValue(quest.progress?.rage ?: 0.0)
} }
fun configure(questContent: QuestContent) { fun configure(questContent: QuestContent) {
this.questContent = questContent this.questContent = questContent
binding.healthBarView.setMaxValue(questContent.boss?.hp?.toDouble() ?: 0.0) 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.bossNameView.text = questContent.boss?.name
binding.typeTextView.text = context.getString(R.string.boss_quest) 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) { fun configure(user: User) {

View file

@ -224,6 +224,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
if (editedTags.containsKey(tag.id)) { if (editedTags.containsKey(tag.id)) {
editedTags.remove(tag.id) editedTags.remove(tag.id)
} }
activeTags.remove(tag.id)
tags.remove(tag) tags.remove(tag)
tagsList.removeView(wrapper) tagsList.removeView(wrapper)
} }

View file

@ -55,6 +55,12 @@ class ReminderContainer @JvmOverloads constructor(
} }
var firstDayOfWeek: Int? = null var firstDayOfWeek: Int? = null
set(value) {
children
.filterIsInstance<ReminderItemFormView>()
.forEach { it.firstDayOfWeek = value }
field = value
}
init { init {
orientation = VERTICAL orientation = VERTICAL

View file

@ -1 +1 @@
Weve improved more of our notifications so they bring you to the relevant screen when tapped. When customizing your avatar, youll 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. Weve fixed a few bugs too, including one where tasks wouldnt load when the app is left running in the background, the stats widget works again, and another where Dailies wouldnt filter properly on launch. In this update weve 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. Weve 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.