refactor for more coroutines

This commit is contained in:
Phillip Thelen 2022-08-09 11:32:13 +02:00
parent df993ec34d
commit 5f5729deca
27 changed files with 181 additions and 111 deletions

View file

@ -12,6 +12,7 @@ import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.flow.Flow
interface SocialRepository : BaseRepository {
fun getPublicGuilds(): Flowable<out List<Group>>
@ -40,7 +41,8 @@ interface SocialRepository : BaseRepository {
fun postGroupChat(groupId: String, message: String): Flowable<PostChatMessageResult>
fun retrieveGroup(id: String): Flowable<Group>
fun getGroup(id: String?): Flowable<Group>
fun getGroup(id: String?): Flow<Group?>
fun getGroupFlowable(id: String?): Flowable<Group>
fun leaveGroup(id: String?, keepChallenges: Boolean): Flowable<Group>
@ -77,7 +79,7 @@ interface SocialRepository : BaseRepository {
fun postPrivateMessage(recipientId: String, message: String): Flowable<List<ChatMessage>>
fun getGroupMembers(id: String): Flowable<out List<Member>>
fun getGroupMembers(id: String): Flow<List<Member>>
fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable<List<Member>>
fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<List<Void>>

View file

@ -17,8 +17,12 @@ import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import java.util.UUID
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.util.UUID
class SocialRepositoryImpl(
localRepository: SocialLocalRepository,
@ -26,7 +30,7 @@ class SocialRepositoryImpl(
userID: String
) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, userID), SocialRepository {
override fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group> {
return localRepository.getGroup(groupID)
return localRepository.getGroupFlowable(groupID)
.map {
val group = localRepository.getUnmanagedCopy(it)
group.leaderID = userID
@ -131,25 +135,23 @@ class SocialRepositoryImpl(
return Flowable.zip(
apiClient.getGroup(id).doOnNext { localRepository.saveGroup(it) },
retrieveGroupChat(id)
.toFlowable(),
{ group, _ ->
group
}
).doOnError {
.toFlowable()
) { group, _ ->
group
}.doOnError {
if (it is HttpException && it.code() == 404) {
localRepository.getGroup(id).firstElement().subscribe { group ->
localRepository.delete(group)
MainScope().launch {
val group = localRepository.getGroup(id).first()
if (group != null) {
localRepository.delete(group)
}
}
}
}
}
override fun getGroup(id: String?): Flowable<Group> {
if (id?.isNotBlank() != true) {
return Flowable.empty()
}
return localRepository.getGroup(id)
}
override fun getGroup(id: String?) = id?.let { localRepository.getGroup(it) } ?: emptyFlow()
override fun getGroupFlowable(id: String?): Flowable<Group> = id?.let { localRepository.getGroupFlowable(it) } ?: Flowable.empty()
override fun leaveGroup(id: String?, keepChallenges: Boolean): Flowable<Group> {
if (id?.isNotBlank() != true) {
@ -157,7 +159,7 @@ class SocialRepositoryImpl(
}
return apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges")
.doOnNext { localRepository.updateMembership(userID, id, false) }
.flatMapMaybe { localRepository.getGroup(id).firstElement() }
.flatMapMaybe { localRepository.getGroupFlowable(id).firstElement() }
}
override fun joinGroup(id: String?): Flowable<Group> {
@ -256,7 +258,7 @@ class SocialRepositoryImpl(
return postPrivateMessage(recipientId, messageObject)
}
override fun getGroupMembers(id: String): Flowable<out List<Member>> = localRepository.getGroupMembers(id)
override fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
override fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable<List<Member>> {
return apiClient.getGroupMembers(id, includeAllPublicFields)

View file

@ -7,6 +7,7 @@ import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.Flow
interface SocialLocalRepository : BaseLocalRepository {
fun getPublicGuilds(): Flowable<out List<Group>>
@ -14,14 +15,15 @@ interface SocialLocalRepository : BaseLocalRepository {
fun getUserGroups(userID: String, type: String?): Flowable<out List<Group>>
fun getGroups(type: String): Flowable<out List<Group>>
fun getGroup(id: String): Flowable<Group>
fun getGroup(id: String): Flow<Group?>
fun getGroupFlowable(id: String): Flowable<Group>
fun saveGroup(group: Group)
fun getGroupChat(groupId: String): Flowable<out List<ChatMessage>>
fun deleteMessage(id: String)
fun getGroupMembers(partyId: String): Flowable<out List<Member>>
fun getGroupMembers(partyId: String): Flow<List<Member>>
fun updateRSVPNeeded(user: User?, newValue: Boolean)

View file

@ -13,6 +13,10 @@ import hu.akarnokd.rxjava3.bridge.RxJavaBridge
import io.reactivex.rxjava3.core.Flowable
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), SocialLocalRepository {
@ -165,7 +169,7 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
)
}
override fun getGroup(id: String): Flowable<Group> {
override fun getGroupFlowable(id: String): Flowable<Group> {
return RxJavaBridge.toV3Flowable(
realm.where(Group::class.java)
.equalTo("id", id)
@ -176,6 +180,15 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
)
}
override fun getGroup(id: String): Flow<Group?> {
return realm.where(Group::class.java)
.equalTo("id", id)
.findAll()
.toFlow()
.filter { group -> group.isLoaded && group.isValid && !group.isEmpty() }
.map { groups -> groups.first() }
}
override fun getGroupChat(groupId: String): Flowable<out List<ChatMessage>> {
return RxJavaBridge.toV3Flowable(
realm.where(ChatMessage::class.java)
@ -192,14 +205,12 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
executeTransaction { chatMessage?.deleteFromRealm() }
}
override fun getGroupMembers(partyId: String): Flowable<out List<Member>> {
return RxJavaBridge.toV3Flowable(
realm.where(Member::class.java)
override fun getGroupMembers(partyId: String): Flow<List<Member>> {
return realm.where(Member::class.java)
.equalTo("party.id", partyId)
.findAll()
.asFlowable()
.toFlow()
.filter { it.isLoaded }
)
}
override fun updateRSVPNeeded(user: User?, newValue: Boolean) {

View file

@ -5,9 +5,9 @@ import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.RemindersItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.common.habitica.models.tasks.TaskType
import com.habitrpg.common.habitica.models.tasks.TasksOrder
import com.habitrpg.android.habitica.models.user.User
import hu.akarnokd.rxjava3.bridge.RxJavaBridge
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
@ -169,8 +169,9 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
return Flowable.empty()
}
return RxJavaBridge.toV3Flowable(
realm.where(Task::class.java).equalTo("id", taskId).findFirstAsync().asFlowable<RealmObject>()
.filter { realmObject -> realmObject.isLoaded }
realm.where(Task::class.java).equalTo("id", taskId).findAll().asFlowable()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.cast(Task::class.java)
)
}
@ -187,8 +188,8 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
}
override fun markTaskCompleted(taskId: String, isCompleted: Boolean) {
val task = realm.where(Task::class.java).equalTo("id", taskId).findFirstAsync()
executeTransaction { task.completed = true }
val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst()
executeTransaction { task?.completed = true }
}
override fun swapTaskPosition(firstPosition: Int, secondPosition: Int) {

View file

@ -59,7 +59,7 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
.equalTo("id", userID)
.findAll()
.toFlow()
.filter { it.isLoaded }
.filter { it.isLoaded && it.size > 0 }
.map { it.first()?.questAchievements ?: emptyList() }
}

View file

@ -46,6 +46,7 @@ class SoundFile(val theme: String, private val fileName: String) {
player?.setVolume(100f, 100f)
player?.isLooping = false
player?.start()
} catch (e: IllegalStateException) {
} catch (e: Exception) {
RxErrorHandler.reportError(e)
}

View file

@ -297,6 +297,8 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
when {
text != task.text -> return true
notes != task.notes -> return true
reminders?.size != task.reminders?.size -> return true
checklist?.size != task.checklist?.size -> return true
reminders?.mapIndexed { index, remindersItem -> task.reminders?.get(index) != remindersItem }?.contains(true) == true -> return true
checklist?.mapIndexed { index, item -> task.checklist?.get(index) != item }?.contains(true) == true -> return true
priority != task.priority -> return true

View file

@ -26,17 +26,17 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.extensions.updateStatusBarColor
import com.habitrpg.common.habitica.helpers.LanguageHelper
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.interactors.ShowNotificationInteractor
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import com.habitrpg.common.habitica.helpers.LanguageHelper
import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.util.Date
import java.util.Locale
@ -245,8 +245,10 @@ abstract class BaseActivity : AppCompatActivity() {
sharingIntent.putExtra(Intent.EXTRA_TEXT, message)
if (image != null) {
val path = MediaStore.Images.Media.insertImage(this.contentResolver, image, "${(Date())}", null)
val uri = Uri.parse(path)
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri)
if (path != null) {
val uri = Uri.parse(path)
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri)
}
}
startActivity(Intent.createChooser(sharingIntent, getString(R.string.share_using)))
}

View file

@ -364,7 +364,8 @@ class FullProfileActivity : BaseActivity() {
binding.equipmentTableLayout.removeAllViews()
for (index in 1 until binding.attributesTableLayout.childCount) {
if (binding.attributesTableLayout.getChildAt(index).isAttachedToWindow) {
val child = binding.attributesTableLayout.getChildAt(index) ?: continue
if (child.isAttachedToWindow) {
binding.attributesTableLayout.removeViewAt(index)
}
}

View file

@ -27,7 +27,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.MessageClient
@ -239,23 +238,27 @@ class LoginActivity : BaseActivity() {
val messageClient: MessageClient = Wearable.getMessageClient(this)
val capabilityClient: CapabilityClient = Wearable.getCapabilityClient(this)
lifecycleScope.launch(Dispatchers.IO) {
val info = Tasks.await(
capabilityClient.getCapability(
"receive_message",
CapabilityClient.FILTER_REACHABLE
)
)
info.nodes.forEach {
Tasks.await(
messageClient.sendMessage(
it.id,
"/auth",
"${response.id}:${response.apiToken}".toByteArray()
try {
val info = Tasks.await(
capabilityClient.getCapability(
"receive_message",
CapabilityClient.FILTER_REACHABLE
)
)
info.nodes.forEach {
Tasks.await(
messageClient.sendMessage(
it.id,
"/auth",
"${response.id}:${response.apiToken}".toByteArray()
)
)
}
} catch (e: Exception) {
// Wearable API is not available on this device.
}
}
} catch (e: ApiException) {
} catch (e: Exception) {
// Wearable API is not available on this device.
}
compositeSubscription.add(

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
@ -12,6 +13,7 @@ import com.habitrpg.android.habitica.databinding.ActivitySkillMembersBinding
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.ui.adapter.social.PartyMemberRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class SkillMemberActivity : BaseActivity() {
@ -56,10 +58,12 @@ class SkillMemberActivity : BaseActivity() {
)?.let { compositeSubscription.add(it) }
binding.recyclerView.adapter = viewAdapter
compositeSubscription.add(
userRepository.getUserFlowable()
.flatMap { user -> socialRepository.getGroupMembers(user.party?.id ?: "") }
.subscribe({ viewAdapter?.data = it }, RxErrorHandler.handleEmptyError())
)
val user = userViewModel.user.value
lifecycleScope.launch {
socialRepository.getGroupMembers(user?.party?.id ?: "")
.collect {
viewAdapter?.data = it
}
}
}
}

View file

@ -110,6 +110,7 @@ class AvatarCustomizationFragment :
}
adapter.customizationType = type
binding?.refreshLayout?.setOnRefreshListener(this)
layoutManager = FlexboxLayoutManager(activity, ROW)
layoutManager.justifyContent = JustifyContent.CENTER
layoutManager.alignItems = AlignItems.FLEX_START
binding?.recyclerView?.layoutManager = layoutManager

View file

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
@ -21,11 +22,13 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.inventory.ShopRecyclerAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.common.habitica.helpers.RecyclerViewState
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.CurrencyViews
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.helpers.RecyclerViewState
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import javax.inject.Inject
open class ShopFragment : BaseMainFragment<FragmentRefreshRecyclerviewBinding>() {
@ -133,18 +136,20 @@ open class ShopFragment : BaseMainFragment<FragmentRefreshRecyclerviewBinding>()
updateCurrencyView(it)
}
compositeSubscription.add(
lifecycleScope.launch {
socialRepository.getGroup(Group.TAVERN_ID)
.filter { it.hasActiveQuest }
.filter { group -> group.quest?.rageStrikes?.any { it.key == shopIdentifier } ?: false }
.filter { group -> group.quest?.rageStrikes?.filter { it.key == shopIdentifier }?.get(0)?.wasHit == true }
.subscribe(
{
adapter?.shopSpriteSuffix = "_" + it.quest?.key
},
RxErrorHandler.handleEmptyError()
)
)
.filter { it?.hasActiveQuest == true }
.filter { group ->
group?.quest?.rageStrikes?.any { it.key == shopIdentifier } ?: false
}
.filter { group ->
group?.quest?.rageStrikes?.filter { it.key == shopIdentifier }
?.get(0)?.wasHit == true
}
.collect {
adapter?.shopSpriteSuffix = "_" + it?.quest?.key
}
}
view.post { setGridSpanCount(view.width) }

View file

@ -24,10 +24,10 @@ import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.common.habitica.helpers.MarkdownParser
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.common.habitica.helpers.MarkdownParser
import javax.inject.Inject
import javax.inject.Named
@ -79,7 +79,7 @@ class QuestDetailFragment : BaseMainFragment<FragmentQuestDetailBinding>() {
}
.skipWhile { it.isBlank() }
.distinctUntilChanged()
.flatMap { socialRepository.getGroup(it) }
.flatMap { socialRepository.getGroupFlowable(it) }
.doOnNext { updateParty(it) }
.map {
it.quest?.key ?: ""

View file

@ -26,9 +26,9 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.UsernameLabel
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.models.PlayerTier
import com.habitrpg.android.habitica.ui.views.UsernameLabel
import javax.inject.Inject
class TavernDetailFragment : BaseFragment<FragmentTavernDetailBinding>() {
@ -74,7 +74,7 @@ class TavernDetailFragment : BaseFragment<FragmentTavernDetailBinding>() {
bindButtons()
compositeSubscription.add(
socialRepository.getGroup(Group.TAVERN_ID)
socialRepository.getGroupFlowable(Group.TAVERN_ID)
.doOnNext { if (!it.hasActiveQuest) binding?.worldBossSection?.visibility = View.GONE }
.filter { it.hasActiveQuest }
.doOnNext {

View file

@ -18,8 +18,8 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.adapter.social.ChallengesListViewAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.common.habitica.helpers.EmptyItem
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.common.habitica.helpers.EmptyItem
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.kotlin.Flowables
import javax.inject.Inject
@ -84,7 +84,7 @@ class ChallengeListFragment : BaseFragment<FragmentRefreshRecyclerviewBinding>()
}
compositeSubscription.add(
Flowables.combineLatest(socialRepository.getGroup(Group.TAVERN_ID), socialRepository.getUserGroups("guild")).subscribe(
Flowables.combineLatest(socialRepository.getGroupFlowable(Group.TAVERN_ID), socialRepository.getUserGroups("guild")).subscribe(
{
this.filterGroups = mutableListOf()
filterGroups?.add(it.first)

View file

@ -20,21 +20,19 @@ import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.social.Challenge
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.activities.GroupInviteActivity
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.common.habitica.helpers.setMarkdown
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaIcons
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.SnackbarActivity
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.helpers.setMarkdown
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Named
class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
@ -49,9 +47,6 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
@Inject
lateinit var userRepository: UserRepository
@field:[Inject Named(AppModule.NAMED_USER_ID)]
lateinit var userId: String
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGuildDetailBinding {
return FragmentGuildDetailBinding.inflate(inflater, container, false)
}

View file

@ -57,8 +57,8 @@ class GuildFragment : BaseMainFragment<FragmentViewpagerBinding>() {
super.onViewCreated(view, savedInstanceState)
viewModel.groupViewType = GroupViewType.GUILD
viewModel.getGroupData().observe(viewLifecycleOwner, { setGroup(it) })
viewModel.getIsMemberData().observe(viewLifecycleOwner, { activity?.invalidateOptionsMenu() })
viewModel.getGroupData().observe(viewLifecycleOwner) { setGroup(it) }
viewModel.getIsMemberData().observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
arguments?.let {
val args = GuildFragmentArgs.fromBundle(it)

View file

@ -144,8 +144,8 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
recyclerAdapter?.adventureGuideOpenEvents?.subscribeWithErrorHandler { MainNavigationController.navigate(R.id.adventureGuideActivity) }?.let { recyclerSubscription.add(it) }
viewModel.ownerID.observe(viewLifecycleOwner) {
canEditTasks = viewModel.isPersonalBoard ?: true
canScoreTaks = viewModel.isPersonalBoard ?: true
canEditTasks = viewModel.isPersonalBoard
canScoreTaks = viewModel.isPersonalBoard
recyclerAdapter?.canScoreTasks = canScoreTaks
updateTaskSubscription(it)
}
@ -270,8 +270,8 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
) {
if (validTaskId != null) {
var newPosition = viewHolder.bindingAdapterPosition
if ((viewModel.filterCount(taskType) ?: 0) > 0) {
newPosition = if ((newPosition + 1) == recyclerAdapter?.data?.size) {
if (viewModel.filterCount(taskType) > 0) {
newPosition = if ((newPosition + 1) >= (recyclerAdapter?.data?.size ?: 0)) {
recyclerAdapter?.data?.get(newPosition - 1)?.position ?: newPosition
} else {
(recyclerAdapter?.data?.get(newPosition + 1)?.position ?: newPosition) - 1
@ -370,7 +370,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
private fun setEmptyLabels() {
binding?.recyclerView?.emptyItem = if ((viewModel.filterCount(taskType) ?: 0) > 0) {
binding?.recyclerView?.emptyItem = if (viewModel.filterCount(taskType) > 0) {
when (this.taskType) {
TaskType.HABIT -> {
EmptyItem(

View file

@ -6,24 +6,24 @@ import androidx.lifecycle.MutableLiveData
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ChallengeRepository
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.common.habitica.extensions.Optional
import com.habitrpg.common.habitica.extensions.asOptional
import com.habitrpg.android.habitica.extensions.filterOptionalDoOnEmpty
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.common.habitica.models.notifications.NewChatMessageData
import com.habitrpg.android.habitica.models.social.Challenge
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.common.habitica.extensions.Optional
import com.habitrpg.common.habitica.extensions.asOptional
import com.habitrpg.common.habitica.models.notifications.NewChatMessageData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.subjects.BehaviorSubject
import retrofit2.HttpException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import retrofit2.HttpException
enum class GroupViewType(internal val order: String) {
PARTY("party"),
@ -121,7 +121,7 @@ open class GroupViewModel(initializeComponent: Boolean) : BaseViewModel(initiali
disposable.add(
groupIDFlowable
.filterOptionalDoOnEmpty { group.value = null }
.flatMap { socialRepository.getGroup(it) }
.flatMap { socialRepository.getGroupFlowable(it) }
.map { socialRepository.getUnmanagedCopy(it) }
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ group.value = it }, RxErrorHandler.handleEmptyError())
@ -132,7 +132,7 @@ open class GroupViewModel(initializeComponent: Boolean) : BaseViewModel(initiali
disposable.add(
groupIDFlowable
.filterOptionalDoOnEmpty { leader.value = null }
.flatMap { socialRepository.getGroup(it) }
.flatMap { socialRepository.getGroupFlowable(it) }
.distinctUntilChanged { group1, group2 -> group1.id == group2.id }
.flatMap { socialRepository.getMember(it.leaderID) }
.observeOn(AndroidSchedulers.mainThread())

View file

@ -2,12 +2,14 @@ package com.habitrpg.android.habitica.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.extensions.filterOptionalDoOnEmpty
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.members.Member
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.BackpressureStrategy
import kotlinx.coroutines.launch
class PartyViewModel(initializeComponent: Boolean) : GroupViewModel(initializeComponent) {
constructor() : this(true)
@ -39,10 +41,16 @@ class PartyViewModel(initializeComponent: Boolean) : GroupViewModel(initializeCo
private fun loadMembersFromLocal() {
disposable.add(
groupIDSubject.toFlowable(BackpressureStrategy.LATEST)
.distinctUntilChanged()
.filterOptionalDoOnEmpty { members.value = null }
.flatMap { socialRepository.getGroupMembers(it) }
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ members.value = it }, RxErrorHandler.handleEmptyError())
.subscribe({
viewModelScope.launch {
socialRepository.getGroupMembers(it)
.collect {
members.value = it
}
}}, RxErrorHandler.handleEmptyError())
)
}

View file

@ -8,6 +8,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.habitrpg.android.habitica.extensions.getAsString
import com.habitrpg.android.habitica.models.social.Challenge
import java.lang.reflect.Type
import java.util.Date
@ -22,10 +23,8 @@ class ChallengeDeserializer : JsonDeserializer<Challenge>, JsonSerializer<Challe
challenge.id = jsonObject.get("id").asString
challenge.name = jsonObject.get("name").asString
if (jsonObject.has("shortName")) {
challenge.shortName = jsonObject.get("shortName").asString
}
challenge.description = jsonObject.get("description").asString
challenge.shortName = jsonObject.getAsString("shortName")
challenge.description = jsonObject.getAsString("description")
challenge.memberCount = jsonObject.get("memberCount").asInt
val prizeElement = jsonObject.get("prize")

View file

@ -50,6 +50,7 @@ object Animations {
}
fun circularReveal(view: View, duration: Long = 300) {
if (!view.isAttachedToWindow) return
val cx = view.width / 2
val cy = view.height / 2
val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()

View file

@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.NullPointerException
object MarkdownParser {
private val cache = sortedMapOf<Int, Spanned>()
@ -97,9 +98,13 @@ object MarkdownParser {
// Adding this space here bc for some reason some markdown is not rendered correctly when the whole string is supposed to be formatted
val result = markwon?.toMarkdown("$text ") ?: SpannableString(text)
cache[hashCode] = result
if (cache.size > 100) {
cache.remove(0)
try {
cache[hashCode] = result
if (cache.size > 100) {
cache.remove(cache.firstKey())
}
} catch (_: NullPointerException) {
// for some reason hashCode seems to be null sometimes.
}
return result
}
@ -114,7 +119,11 @@ object MarkdownParser {
}
fun hasCached(input: String?): Boolean {
return cache.containsKey(input?.hashCode())
return try {
cache.containsKey(input?.hashCode())
} catch (_: NullPointerException) {
false
}
}
/**

View file

@ -1,3 +1,24 @@
Treat your life like a game to stay motivated and organized! Habitica makes it simple to have fun while accomplishing goals.
Input your Habits, your Daily goals, and your To-Do list, and then create a custom avatar. Check off tasks to level up your avatar and unlock features such as armor, pets, skills, and even quests! Fight monsters with friends to keep each other accountable, and use your gold on in-game rewards, like equipment, or custom awards, like watching an episode of your favorite TV show. Flexible, social, and fun, Habitica is the perfect way to motivate yourself to accomplish anything.
If you have any questions, feel free to send feedback to mobile@habitica.com! And if you enjoy our app, we would really appreciate it if you would leave us a review.
Habitica is a free habit-building and productivity app that uses retro RPG elements to gamify your tasks and goals.
Use Habitica to help with ADHD, self care, New Years resolutions, household chores, work tasks, creative projects, fitness goals, back-to-school routines, and more!
How it works:
Create an avatar then add tasks, chores, or goals youd like to work on. When you do something in real life, check it off in the app and receive gold, experience, and items that can be used in-game!
Features:
• Automatically repeating tasks scheduled for your daily, weekly, or monthly routines
• Flexible habit tracker for tasks you want to do multiple times a day or only once in awhile
• Traditional to do list for tasks that only need to be done once
• Color coded tasks and streak counters help you see how youre doing at a glance
• Leveling system to visualize your overall progress
• Tons of collectable gear and pets to suit your personal style
• Inclusive avatar customizations: wheelchairs, hair styles, skin tones, and more
• Regular content releases and seasonal events to keep things fresh
• Parties let you team up with friends for extra accountability and battle fierce foes by completing tasks
• Challenges offer shared task lists you can add to your personal tasks
• Guilds let you connect with others that share your interests and goals
• Reminders and widgets to help keep you on track
• Customizable color themes with dark and light mode
• Syncing across devices
• Brand new WearOS watch app available in version 4.0!
Habitica is an open-source app run by a small team thats made better by the work of volunteers who contribute pixel art, translations, bug fixes, and more. If youd like to contribute, reach out!
Community, privacy, and transparency are important to us. Your tasks are private and we dont sell your personal data to third parties.
If you have any questions, feel free to send feedback to admin@habitica.com! And if you enjoy our app, we would really appreciate it if you would leave us a review.

View file

@ -1,2 +1,2 @@
NAME=4.0
CODE=4310
NAME=4.0.1
CODE=4330