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'
//Dependency Injection
implementation 'com.google.dagger:dagger:2.38'
kapt 'com.google.dagger:dagger-compiler:2.38'
implementation 'com.google.dagger:dagger:2.39.1'
kapt 'com.google.dagger:dagger-compiler:2.39.1'
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
compileOnly 'com.github.pengrad:jdk9-deps:1.0'
//App Compatibility and Material Design
@ -82,8 +82,8 @@ dependencies {
//Analytics
implementation 'com.amplitude:android-sdk:2.30.0'
// Image Management Library
implementation("io.coil-kt:coil:1.2.2")
implementation("io.coil-kt:coil-gif:1.2.2")
implementation("io.coil-kt:coil:1.4.0")
implementation("io.coil-kt:coil-gif:1.4.0")
//Tests
testImplementation 'io.kotest:kotest-runner-junit5:4.6.2'
@ -116,10 +116,12 @@ dependencies {
implementation 'com.nex3z:flow-layout:1.2.2'
implementation 'androidx.core:core-ktx:1.6.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation "androidx.paging:paging-runtime-ktx:3.0.1"
implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
@ -153,8 +155,8 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\""
resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW"
versionCode 3052
versionName "3.4"
versionCode 3070
versionName "3.4.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View file

@ -36,9 +36,8 @@
android:textSize="12sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/bottomView"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_height="28dp"
android:orientation="horizontal"
android:gravity="center_vertical"
tools:background="?colorPrimaryDark"
@ -73,4 +72,28 @@
tools:text="+1"
/>
</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>

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_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="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>
</resources>
<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>

View file

@ -1114,7 +1114,7 @@
<string name="how_it_works">How it works</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_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="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>
@ -1183,4 +1183,5 @@
<string name="terms_of_service">Terms of Service</string>
<string name="damage_pending">%.01f dmg pending</string>
<string name="x_remaining">%s remaining</string>
<string name="sale_ends_in">Sale ends in %s</string>
</resources>

View file

@ -6,9 +6,11 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
@ -31,6 +33,7 @@ import com.habitrpg.android.habitica.api.HostConfig
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.helpers.LanguageHelper
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import com.habitrpg.android.habitica.modules.UserModule
@ -46,6 +49,7 @@ import org.solovyev.android.checkout.Billing
import org.solovyev.android.checkout.Cache
import org.solovyev.android.checkout.Checkout
import org.solovyev.android.checkout.PurchaseVerifier
import java.util.Locale
import javax.inject.Inject
// contains all HabiticaApplicationLogic except dagger componentInitialisation
@ -78,6 +82,7 @@ abstract class HabiticaBaseApplication : Application() {
super.onCreate()
setupRealm()
setupDagger()
setLocale()
setupRemoteConfig()
setupNotifications()
createBillingAndCheckout()
@ -117,6 +122,21 @@ abstract class HabiticaBaseApplication : Application() {
checkIfNewVersion()
}
private fun setLocale() {
val resources = resources
val configuration: Configuration = resources.configuration
val languageHelper = LanguageHelper(sharedPrefs.getString("language", "en"))
if (if (SDK_INT >= Build.VERSION_CODES.N) {
configuration.locales.isEmpty || configuration.locales[0] != languageHelper.locale
} else {
configuration.locale != languageHelper.locale
}
) {
configuration.setLocale(languageHelper.locale)
resources.updateConfiguration(configuration, null)
}
}
protected open fun setupRealm() {
Realm.init(this)
val builder = RealmConfiguration.Builder()

View file

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

View file

@ -218,7 +218,7 @@ interface ApiService {
fun seenMessages(@Path("gid") groupId: String): Flowable<HabitResponse<Void>>
@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")
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.InboxViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.inventory.equipment.EquipmentOverviewViewModel;
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog;
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog;
import com.habitrpg.android.habitica.ui.views.social.ChatBarView;
@ -346,4 +347,6 @@ public interface UserComponent {
void inject(@NotNull PromoWebFragment promoWebFragment);
void inject(@NotNull ItemDialogFragment itemDialogFragment);
void inject(@NotNull EquipmentOverviewViewModel equipmentOverviewViewModel);
}

View file

@ -153,7 +153,7 @@ interface ApiClient {
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>

View file

@ -51,7 +51,7 @@ interface SocialRepository : BaseRepository {
fun getGroupMembers(id: String): Flowable<out 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 getMemberWithUsername(username: String?): Flowable<Member>
@ -60,6 +60,8 @@ interface SocialRepository : BaseRepository {
fun markPrivateMessagesRead(user: User?): Flowable<Void>
fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>)
fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group>
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.Server
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.events.ConsumablePurchasedEvent
import com.habitrpg.android.habitica.events.ShowConnectionProblemEvent
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.models.*
@ -554,7 +553,7 @@ class ApiClientImpl // private OnHabitsAPIResult mResultListener;
return apiService.seenMessages(groupId).compose(configureApiCallObserver())
}
override fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): Flowable<Void> {
override fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): Flowable<List<Void>> {
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) }
}
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> {
return if (userId == null) {
@ -264,12 +264,31 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
}
override fun markPrivateMessagesRead(user: User?): Flowable<Void> {
if (user?.isManaged == true) {
localRepository.modify(user) {
it.inbox?.hasUserSeenInbox = true
}
}
return apiClient.markPrivateMessagesRead()
.doOnNext {
if (user?.isManaged == true) {
localRepository.modify(user) { it.inbox?.newMessages = 0 }
}
override fun markSomePrivateMessagesAsRead(user: User?, messages: List<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)

View file

@ -64,6 +64,13 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
override fun saveInboxMessages(userID: String, recipientID: String, messages: List<ChatMessage>, page: Int) {
messages.forEach { it.userID = userID }
for (message in messages) {
val existingMessage = realm.where(ChatMessage::class.java)
.equalTo("id", message.id)
.findAll()
.firstOrNull()
message.isSeen = existingMessage != null
}
save(messages)
if (page != 0) return
val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).equalTo("uuid", recipientID).findAll()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,4 +10,5 @@ open class Inbox : RealmObject(), BaseObject {
var optOut: Boolean = false
var blocks: RealmList<String> = RealmList()
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.Context
import android.content.Intent
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.widget.Toast
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput
import com.habitrpg.android.habitica.HabiticaBaseApplication
@ -13,37 +16,44 @@ import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.interactors.NotifyUserUseCase
import com.habitrpg.android.habitica.models.user.User
import javax.inject.Inject
class LocalNotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var userRepository: UserRepository
@Inject
lateinit var socialRepository: SocialRepository
@Inject
lateinit var taskRepository: TaskRepository
@Inject
lateinit var apiClient: ApiClient
private var user: User? = null
private var groupID: String? = null
private var senderID: String? = null
private val groupID: String?
get() = intent?.extras?.getString("groupID")
private val senderID: String?
get() = intent?.extras?.getString("senderID")
private val taskID: String?
get() = intent?.extras?.getString("taskID")
private var context: Context? = null
private var intent: Intent? = null
override fun onReceive(context: Context, intent: Intent) {
HabiticaBaseApplication.userComponent?.inject(this)
this.intent = intent
groupID = intent.extras?.getString("groupID")
senderID = intent.extras?.getString("senderID")
this.context = context
handleLocalNotificationAction(intent.action)
}
private fun handleLocalNotificationAction(action: String?) {
val notificationManager = this.context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.cancelAll()
val notificationManager =
this.context?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.cancel(intent?.extras?.getInt("NOTIFICATION_ID") ?: -1)
when (action) {
context?.getString(R.string.accept_party_invite) -> {
groupID?.let {
@ -52,7 +62,8 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
}
context?.getString(R.string.reject_party_invite) -> {
groupID?.let {
socialRepository.rejectGroupInvite(it).subscribe({ }, RxErrorHandler.handleEmptyError())
socialRepository.rejectGroupInvite(it)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}
context?.getString(R.string.accept_quest_invite) -> {
@ -68,7 +79,8 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
}
context?.getString(R.string.reject_guild_invite) -> {
groupID?.let {
socialRepository.rejectGroupInvite(it).subscribe({ }, RxErrorHandler.handleEmptyError())
socialRepository.rejectGroupInvite(it)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}
context?.getString(R.string.group_message_reply) -> {
@ -76,7 +88,9 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
getMessageText(context?.getString(R.string.group_message_reply))?.let { message ->
socialRepository.postGroupChat(it, message).subscribe(
{
context?.let { c -> NotificationManagerCompat.from(c).cancel(it.hashCode()) }
context?.let { c ->
NotificationManagerCompat.from(c).cancel(it.hashCode())
}
},
RxErrorHandler.handleEmptyError()
)
@ -86,19 +100,33 @@ class LocalNotificationActionReceiver : BroadcastReceiver() {
context?.getString(R.string.inbox_message_reply) -> {
senderID?.let {
getMessageText(context?.getString(R.string.inbox_message_reply))?.let { message ->
socialRepository.postPrivateMessage(it, message).subscribe({ }, RxErrorHandler.handleEmptyError())
socialRepository.postPrivateMessage(it, message)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}
}
context?.getString(R.string.complete_task_action) -> {
intent?.extras?.getString("taskID")?.let {
taskID?.let {
taskRepository.taskChecked(null, it, up = true, force = false) {
}.subscribe({}, RxErrorHandler.handleEmptyError())
}.subscribe({
val pair = NotifyUserUseCase.getNotificationAndAddStatsToUserAsText(
it?.experienceDelta,
it?.healthDelta,
it?.goldDelta,
it?.manaDelta
)
showToast(pair.first)
}, RxErrorHandler.handleEmptyError())
}
}
}
}
private fun showToast(text: Spannable) {
val toast = Toast.makeText(context, text, Toast.LENGTH_LONG)
toast.show()
}
private fun getMessageText(key: String?): String? {
return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(key)?.toString()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -88,7 +88,12 @@ class InboxMessageListFragment : BaseMainFragment<FragmentInboxMessageListBindin
setReceivingUser(member.username, member.id)
activity?.title = member.displayName
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?.itemAnimator = SafeDefaultItemAnimator()
@ -165,6 +170,10 @@ class InboxMessageListFragment : BaseMainFragment<FragmentInboxMessageListBindin
component.inject(this)
}
private fun markMessagesAsRead(messages: List<ChatMessage>) {
socialRepository.markSomePrivateMessagesAsRead(user, messages)
}
private fun startAutoRefreshing() {
if (refreshDisposable != null && refreshDisposable?.isDisposed != true) {
refreshDisposable?.dispose()

View file

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

View file

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

View file

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

View file

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

View file

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

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 {
val currentDialog = dialogQueue.firstOrNull() ?: return true
if (currentDialog.isShowing) {
return false
return if (currentDialog.isShowing && currentDialog.getActivity()?.isFinishing != true) {
false
} else {
// The Dialog was probably dismissed in a weird way. Clear it out so that the queue doesn't get stuck
dialogQueue.removeAt(0)
return true
true
}
}
}

View file

@ -36,6 +36,7 @@ class QuestMenuView : LinearLayout {
orientation = VERTICAL
binding.heartIconView.setImageBitmap(HabiticaIconsHelper.imageOfHeartDarkBg())
binding.rageIconView.setImageBitmap(HabiticaIconsHelper.imageOfRage())
binding.pendingDamageIconView.setImageBitmap(HabiticaIconsHelper.imageOfDamage())
@ -50,16 +51,21 @@ class QuestMenuView : LinearLayout {
fun configure(quest: Quest) {
binding.healthBarView.setCurrentValue(quest.progress?.hp ?: 0.0)
binding.rageBarView.setCurrentValue(quest.progress?.rage ?: 0.0)
}
fun configure(questContent: QuestContent) {
this.questContent = questContent
binding.healthBarView.setMaxValue(questContent.boss?.hp?.toDouble() ?: 0.0)
binding.bottomView.setBackgroundColor(questContent.colors?.darkColor ?: 0)
//binding.bossArtView.setBackgroundColor(questContent.colors?.mediumColor ?: 0)
//DataBindingUtils.loadImage(binding.bossArtView, "quest_" + questContent.key)
binding.bossNameView.text = questContent.boss?.name
binding.typeTextView.text = context.getString(R.string.boss_quest)
if (questContent.boss?.hasRage == true) {
binding.rageView.visibility = View.VISIBLE
binding.rageBarView.setMaxValue(questContent.boss?.rage?.value ?: 0.0)
} else {
binding.rageView.visibility = View.GONE
}
}
fun configure(user: User) {

View file

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

View file

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