mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-23 06:07:16 +00:00
Merge branch 'develop' of github.com:HabitRPG/habitica-android into develop
This commit is contained in:
commit
4790a8dccb
53 changed files with 492 additions and 245 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1098,5 +1098,5 @@
|
|||
<string name="spooky_promo_info_prompt">The Gem Sale is back to haunt the very end of this year’s Fall Gala! This is one last chance to get more Gems than ever, so stock up while it lasts!</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class EquipmentDetailFragment :
|
|||
}
|
||||
|
||||
var type: String? = null
|
||||
var typeText: String? = null
|
||||
var equippedGear: String? = null
|
||||
var isCostume: Boolean? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class ChatFragment : BaseFragment<FragmentChatBinding>() {
|
|||
viewModel?.updateUser("flags.communityGuidelinesAccepted", true)
|
||||
}
|
||||
|
||||
viewModel?.getUserData()?.observe(
|
||||
viewModel?.user?.observe(
|
||||
viewLifecycleOwner,
|
||||
{
|
||||
chatAdapter?.user = it
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ?: "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
We’ve improved more of our notifications so they bring you to the relevant screen when tapped. When customizing your avatar, you’ll be able to see your avatar updating in real time as you select different options. You can also see your current Gems when gifting Gems from your balance now. We’ve fixed a few bugs too, including one where tasks wouldn’t load when the app is left running in the background, the stats widget works again, and another where Dailies wouldn’t filter properly on launch.
|
||||
In this update we’ve fixed some bugs, added more seasonal event support, and made a few quality of life improvements! Sending Guild and Party invites through email should work more reliably. We’ve improved task drag and drop logic when filters are applied to make the order more consistent. Habit streak is now referred to as ‘Habit Counter’ to better reflect the actual behavior. Issues with subscriptions not cancelling fully or getting errors when buying multiple Gem packages should be resolved.
|
||||
|
|
|
|||
Loading…
Reference in a new issue