diff --git a/Habitica/build.gradle b/Habitica/build.gradle index e7ffd5c36..767a68c33 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -132,7 +132,10 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:2.0.0" implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0' implementation 'android.arch.navigation:navigation-ui-ktx:1.0.0' + implementation "androidx.paging:paging-runtime-ktx:2.1.0" implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0' implementation project(':shared') } diff --git a/Habitica/res/layout/shop_header.xml b/Habitica/res/layout/shop_header.xml index 0a3b09b4c..84951777e 100644 --- a/Habitica/res/layout/shop_header.xml +++ b/Habitica/res/layout/shop_header.xml @@ -1,11 +1,10 @@ - + android:layout_height="wrap_content"> - \ No newline at end of file + \ No newline at end of file diff --git a/Habitica/res/navigation/navigation.xml b/Habitica/res/navigation/navigation.xml index 26bf828bd..099163fdf 100644 --- a/Habitica/res/navigation/navigation.xml +++ b/Habitica/res/navigation/navigation.xml @@ -154,7 +154,7 @@ android:label="@string/sidebar_about" /> > - @get:GET("inbox/messages") - val inboxMessages: Flowable>> + @GET("inbox/messages") + fun getInboxMessages(@Query("conversation") uuid: String, @Query("page") page: Int): Flowable>> + @GET("inbox/conversations") + fun getInboxConversations(): Flowable>> @get:GET("tasks/user") diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java b/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java index bf5017a78..051065f41 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/components/UserComponent.java @@ -77,7 +77,7 @@ import com.habitrpg.android.habitica.ui.fragments.social.GroupInformationFragmen import com.habitrpg.android.habitica.ui.fragments.social.GuildDetailFragment; import com.habitrpg.android.habitica.ui.fragments.social.GuildFragment; import com.habitrpg.android.habitica.ui.fragments.social.GuildsOverviewFragment; -import com.habitrpg.android.habitica.ui.fragments.social.InboxFragment; +import com.habitrpg.android.habitica.ui.fragments.social.InboxOverviewFragment; import com.habitrpg.android.habitica.ui.fragments.social.InboxMessageListFragment; import com.habitrpg.android.habitica.ui.fragments.social.PublicGuildsFragment; import com.habitrpg.android.habitica.ui.fragments.social.QuestDetailFragment; @@ -92,6 +92,7 @@ import com.habitrpg.android.habitica.ui.fragments.social.party.PartyInviteFragme import com.habitrpg.android.habitica.ui.fragments.tasks.TaskRecyclerViewFragment; import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment; 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.views.shops.PurchaseDialog; import com.habitrpg.android.habitica.ui.views.social.ChatBarView; @@ -200,7 +201,7 @@ public interface UserComponent { void inject(PreferencesFragment preferencesFragment); - void inject(InboxFragment inboxFragment); + void inject(InboxOverviewFragment inboxFragment); void inject(InboxMessageListFragment inboxMessageListFragment); @@ -313,4 +314,6 @@ public interface UserComponent { void inject(@NotNull GuildDetailFragment guildDetailFragment); void inject(@NotNull AchievementsFragment achievementsFragment); + + void inject(@NotNull InboxViewModel inboxViewModel); } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt index 0434c2138..061d4f243 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt @@ -9,10 +9,7 @@ import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.responses.* import com.habitrpg.android.habitica.models.shops.Shop import com.habitrpg.android.habitica.models.shops.ShopItem -import com.habitrpg.android.habitica.models.social.Challenge -import com.habitrpg.android.habitica.models.social.ChatMessage -import com.habitrpg.android.habitica.models.social.FindUsernameResult -import com.habitrpg.android.habitica.models.social.Group +import com.habitrpg.android.habitica.models.social.* import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.models.tasks.TaskList import com.habitrpg.android.habitica.models.user.Items @@ -223,7 +220,8 @@ interface ApiClient { fun hasAuthenticationKeys(): Boolean fun retrieveUser(withTasks: Boolean): Flowable - fun retrieveInboxMessages(): Flowable> + fun retrieveInboxMessages(uuid: String, page: Int): Flowable> + fun retrieveInboxConversations(): Flowable> fun configureApiCallObserver(): FlowableTransformer, T> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt index 93ed41789..ed4a2bb61 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt @@ -4,10 +4,7 @@ import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.responses.PostChatMessageResult -import com.habitrpg.android.habitica.models.social.ChatMessage -import com.habitrpg.android.habitica.models.social.FindUsernameResult -import com.habitrpg.android.habitica.models.social.Group -import com.habitrpg.android.habitica.models.social.GroupMembership +import com.habitrpg.android.habitica.models.social.* import com.habitrpg.android.habitica.models.user.User import io.reactivex.Flowable import io.reactivex.Single @@ -47,9 +44,10 @@ interface SocialRepository : BaseRepository { fun getInboxMessages(replyToUserID: String?): Flowable> - fun retrieveInboxMessages(): Flowable> - fun getInboxOverviewList(): Flowable> - fun postPrivateMessage(messageObject: HashMap): Flowable> + fun retrieveInboxMessages(uuid: String, page: Int): Flowable> + fun retrieveInboxConversations(): Flowable> + fun getInboxConversations(): Flowable> + fun postPrivateMessage(recipientId: String, messageObject: HashMap): Flowable> fun postPrivateMessage(recipientId: String, message: String): Flowable> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt index c53e62e96..e36b57256 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt @@ -25,10 +25,7 @@ import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.responses.* import com.habitrpg.android.habitica.models.shops.Shop import com.habitrpg.android.habitica.models.shops.ShopItem -import com.habitrpg.android.habitica.models.social.Challenge -import com.habitrpg.android.habitica.models.social.ChatMessage -import com.habitrpg.android.habitica.models.social.FindUsernameResult -import com.habitrpg.android.habitica.models.social.Group +import com.habitrpg.android.habitica.models.social.* import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.models.tasks.TaskList import com.habitrpg.android.habitica.models.user.Items @@ -245,8 +242,12 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener; return userObservable } - override fun retrieveInboxMessages(): Flowable> { - return apiService.inboxMessages.compose(configureApiCallObserver()) + override fun retrieveInboxMessages(uuid: String, page: Int): Flowable> { + return apiService.getInboxMessages(uuid, page).compose(configureApiCallObserver()) + } + + override fun retrieveInboxConversations(): Flowable> { + return apiService.getInboxConversations().compose(configureApiCallObserver()) } override fun hasAuthenticationKeys(): Boolean { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt index b308087b2..ae39855aa 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt @@ -8,10 +8,7 @@ import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.responses.PostChatMessageResult -import com.habitrpg.android.habitica.models.social.ChatMessage -import com.habitrpg.android.habitica.models.social.FindUsernameResult -import com.habitrpg.android.habitica.models.social.Group -import com.habitrpg.android.habitica.models.social.GroupMembership +import com.habitrpg.android.habitica.models.social.* import com.habitrpg.android.habitica.models.user.User import io.reactivex.Flowable import io.reactivex.Single @@ -201,28 +198,34 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap override fun getPublicGuilds(): Flowable> = localRepository.getPublicGuilds() - override fun getInboxOverviewList(): Flowable> = localRepository.getInboxOverviewList(userID) + override fun getInboxConversations(): Flowable> = localRepository.getInboxConversation(userID) override fun getInboxMessages(replyToUserID: String?): Flowable> = localRepository.getInboxMessages(userID, replyToUserID) - override fun retrieveInboxMessages(): Flowable> { - return apiClient.retrieveInboxMessages().doOnNext { messages -> + override fun retrieveInboxMessages(uuid: String, page: Int): Flowable> { + return apiClient.retrieveInboxMessages(uuid, page).doOnNext { messages -> messages.forEach { it.isInboxMessage = true } - localRepository.saveInboxMessages(userID, messages) + localRepository.saveInboxMessages(userID, uuid, messages, page) } } - override fun postPrivateMessage(messageObject: HashMap): Flowable> { - return apiClient.postPrivateMessage(messageObject).flatMap { retrieveInboxMessages() } + override fun retrieveInboxConversations(): Flowable> { + return apiClient.retrieveInboxConversations().doOnNext { conversations -> + localRepository.saveInboxConversations(userID, conversations) + } + } + + override fun postPrivateMessage(recipientId: String, messageObject: HashMap): Flowable> { + return apiClient.postPrivateMessage(messageObject).flatMap { retrieveInboxMessages(recipientId, 0) } } override fun postPrivateMessage(recipientId: String, message: String): Flowable> { val messageObject = HashMap() messageObject["message"] = message messageObject["toUserId"] = recipientId - return postPrivateMessage(messageObject) + return postPrivateMessage(recipientId, messageObject) } override fun getGroupMembers(id: String): Flowable> = localRepository.getGroupMembers(id) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/SocialLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/SocialLocalRepository.kt index 0d45ad128..542428130 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/SocialLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/SocialLocalRepository.kt @@ -4,6 +4,7 @@ import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.models.social.Group 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.Flowable import io.realm.RealmResults @@ -42,8 +43,9 @@ interface SocialLocalRepository : BaseLocalRepository { fun getInboxMessages(userId: String, replyToUserID: String?): Flowable> - fun getInboxOverviewList(userId: String): Flowable> + fun getInboxConversation(userId: String): Flowable> fun saveGroupMemberships(userID: String?, memberships: List) - fun saveInboxMessages(userID: String, messages: List) + fun saveInboxMessages(userID: String, recipientID: String, messages: List, page: Int) + fun saveInboxConversations(userID: String, conversations: List) fun getChatMessage(messageID: String): Flowable } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt index ffa20a5d3..074805afa 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/local/implementation/RealmSocialLocalRepository.kt @@ -49,9 +49,11 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm) } } - override fun saveInboxMessages(userID: String, messages: List) { + override fun saveInboxMessages(userID: String, recipientID: String, messages: List, page: Int) { + messages.forEach { it.userID = userID } realm.executeTransaction { realm.insertOrUpdate(messages) } - val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).findAll() + if (page != 0) return + val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).equalTo("uuid", recipientID).findAll() val messagesToRemove = ArrayList() for (existingMessage in existingMessages) { val isStillMember = messages.any { existingMessage.id == it.id } @@ -64,6 +66,22 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm) } } + override fun saveInboxConversations(userID: String, conversations: List) { + conversations.forEach { it.userID = userID } + realm.executeTransaction { realm.insertOrUpdate(conversations) } + val existingConversations = realm.where(InboxConversation::class.java).findAll() + val conversationsToRemove = ArrayList() + for (existingMessage in existingConversations) { + val isStillMember = conversations.any { existingMessage.uuid == it.uuid } + if (!isStillMember) { + conversationsToRemove.add(existingMessage) + } + } + realm.executeTransaction { + conversationsToRemove.forEach { it.deleteFromRealm() } + } + } + override fun saveGroupMemberships(userID: String?, memberships: List) { realm.executeTransaction { realm.insertOrUpdate(memberships) } if (userID != null) { @@ -238,21 +256,21 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm) return party != null && party.isValid } - override fun getInboxMessages(userId: String, replyToUserID: String?): Flowable> { + override fun getInboxMessages(userID: String, replyToUserID: String?): Flowable> { return realm.where(ChatMessage::class.java) .equalTo("isInboxMessage", true) .equalTo("uuid", replyToUserID) + .equalTo("userID", userID) .sort("timestamp", Sort.DESCENDING) .findAll() .asFlowable() .filter { it.isLoaded } } - override fun getInboxOverviewList(userId: String): Flowable> { - return realm.where(ChatMessage::class.java) - .equalTo("isInboxMessage", true) + override fun getInboxConversation(userID: String): Flowable> { + return realm.where(InboxConversation::class.java) + .equalTo("userID", userID) .sort("timestamp", Sort.DESCENDING) - .distinct("uuid") .findAll() .asFlowable() .filter { it.isLoaded } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt index b23f6ccf4..63f4f4cad 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt @@ -1,9 +1,13 @@ package com.habitrpg.android.habitica.extensions +import android.content.res.Resources +import com.habitrpg.android.habitica.R +import java.util.* + class DateUtils { companion object { - fun minutesInMs(minutes: Int): Int { + private fun minutesInMs(minutes: Int): Int { return minutes * 60 * 1000 } @@ -13,5 +17,25 @@ class DateUtils { } } +fun Date.getAgoString(res: Resources): String { + return this.time.getAgoString(res) +} +fun Long.getAgoString(res: Resources): String { + val diff = Date().time - this + val diffMinutes = diff / (60 * 1000) % 60 + val diffHours = diff / (60 * 60 * 1000) % 24 + val diffDays = diff / (24 * 60 * 60 * 1000) + + return when { + diffDays != 0L -> if (diffDays == 1L) { + res.getString(R.string.ago_1day) + } else res.getString(R.string.ago_days, diffDays) + diffHours != 0L -> if (diffHours == 1L) { + res.getString(R.string.ago_1hour) + } else res.getString(R.string.ago_hours, diffHours) + diffMinutes == 1L -> res.getString(R.string.ago_1Minute) + else -> res.getString(R.string.ago_minutes, diffMinutes) + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt index 009d0812b..5e3887fa2 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt @@ -1,12 +1,6 @@ package com.habitrpg.android.habitica.models.social -import android.content.res.Resources - -import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.models.user.ContributorInfo - -import java.util.Date - import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.Ignore @@ -35,6 +29,7 @@ open class ChatMessage : RealmObject() { var flagCount: Int = 0 var uuid: String? = null + var userID: String? = null var contributor: ContributorInfo? = null @@ -60,25 +55,6 @@ open class ChatMessage : RealmObject() { val formattedUsername: String? get() = if (username != null) "@$username" else null - fun getAgoString(res: Resources): String { - val diff = Date().time - (timestamp ?: 0) - - val diffMinutes = diff / (60 * 1000) % 60 - val diffHours = diff / (60 * 60 * 1000) % 24 - val diffDays = diff / (24 * 60 * 60 * 1000) - - return when { - diffDays != 0L -> if (diffDays == 1L) { - res.getString(R.string.ago_1day) - } else res.getString(R.string.ago_days, diffDays) - diffHours != 0L -> if (diffHours == 1L) { - res.getString(R.string.ago_1hour) - } else res.getString(R.string.ago_hours, diffHours) - diffMinutes == 1L -> res.getString(R.string.ago_1Minute) - else -> res.getString(R.string.ago_minutes, diffMinutes) - } - } - fun userLikesMessage(userId: String?): Boolean { return likes?.any { userId == it.id } ?: false } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/InboxConversation.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/InboxConversation.kt new file mode 100644 index 000000000..f440309e7 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/InboxConversation.kt @@ -0,0 +1,31 @@ +package com.habitrpg.android.habitica.models.social + +import com.habitrpg.android.habitica.models.user.ContributorInfo +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import java.util.* + +open class InboxConversation : RealmObject() { + + @PrimaryKey + var combinedID: String = "" + var uuid: String = "" + set(value) { + field = value + combinedID = userID + value + } + var userID: String = "" + set(value) { + field = value + combinedID = value + uuid + } + var username: String? = null + var user: String? = null + var timestamp: Date? = null + var contributor: ContributorInfo? = null + var userStyles: UserStyles? = null + var text: String? = null + + val formattedUsername: String? + get() = if (username?.isNotEmpty() == true) "@$username" else null +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt index 092232599..afb4dad1e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt @@ -1,35 +1,17 @@ package com.habitrpg.android.habitica.ui.adapter.social -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Resources -import android.graphics.drawable.BitmapDrawable -import android.text.method.LinkMovementMethod import android.view.View import android.view.ViewGroup -import android.widget.Button -import android.widget.LinearLayout import android.widget.TextView -import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.extensions.dpToPx import com.habitrpg.android.habitica.extensions.inflate -import com.habitrpg.android.habitica.extensions.setScaledPadding import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.models.user.User -import com.habitrpg.android.habitica.ui.AvatarView -import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils -import com.habitrpg.android.habitica.ui.helpers.MarkdownParser import com.habitrpg.android.habitica.ui.helpers.bindView -import com.habitrpg.android.habitica.ui.views.HabiticaEmojiTextView -import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper -import com.habitrpg.android.habitica.ui.views.social.UsernameLabel +import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerViewHolder import io.reactivex.BackpressureStrategy import io.reactivex.Flowable -import io.reactivex.Maybe -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.PublishSubject import io.realm.OrderedRealmCollection import io.realm.RealmRecyclerViewAdapter @@ -63,11 +45,23 @@ class ChatRecyclerViewAdapter(data: OrderedRealmCollection?, autoUp } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - data?.let { - if (it[position].isSystemMessage) { - (holder as? SystemChatMessageViewHolder)?.bind(it[position]) + data?.let { data -> + if (data[position].isSystemMessage) { + (holder as? SystemChatMessageViewHolder)?.bind(data[position]) } else { - (holder as? ChatRecyclerViewHolder)?.bind(it[position], uuid) + val chatHolder = holder as? ChatRecyclerViewHolder ?: return + val message = data[position] + chatHolder.bind(message, + uuid, + user, + expandedMessageId == message.id) + chatHolder.onShouldExpand = { expandMessage(message.id, position) } + chatHolder.onLikeMessage = { likeMessageEvents.onNext(it) } + chatHolder.onOpenProfile = { userLabelClickEvents.onNext(it) } + chatHolder.onReply = { replyMessageEvents.onNext(it) } + chatHolder.onCopyMessage = { copyMessageEvents.onNext(it) } + chatHolder.onFlagMessage = { flagMessageEvents.onNext(it) } + chatHolder.onDeleteMessage = { deleteMessageEvents.onNext(it) } } } } @@ -100,195 +94,21 @@ class ChatRecyclerViewAdapter(data: OrderedRealmCollection?, autoUp return copyMessageEvents.toFlowable(BackpressureStrategy.DROP) } - inner class SystemChatMessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val textView: TextView by bindView(R.id.text_view) - - fun bind(chatMessage: ChatMessage?) { - textView.text = chatMessage?.text?.removePrefix("`")?.removeSuffix("`") - } - - } - - inner class ChatRecyclerViewHolder(itemView: View, private var userId: String, private val isTavern: Boolean) : RecyclerView.ViewHolder(itemView) { - - private val messageWrapper: ViewGroup by bindView(R.id.message_wrapper) - private val avatarView: AvatarView by bindView(R.id.avatar_view) - private val userLabel: UsernameLabel by bindView(R.id.user_label) - private val messageText: HabiticaEmojiTextView by bindView(R.id.message_text) - private val sublineTextView: TextView by bindView(R.id.subline_textview) - private val likeBackground: LinearLayout by bindView(R.id.like_background_layout) - private val tvLikes: TextView by bindView(R.id.tvLikes) - private val buttonsWrapper: ViewGroup by bindView(R.id.buttons_wrapper) - private val replyButton: Button by bindView(R.id.reply_button) - private val copyButton: Button by bindView(R.id.copy_button) - private val reportButton: Button by bindView(R.id.report_button) - private val deleteButton: Button by bindView(R.id.delete_button) - private val modView: TextView by bindView(R.id.mod_view) - - val context: Context = itemView.context - val res: Resources = itemView.resources - private var chatMessage: ChatMessage? = null - - init { - itemView.setOnClickListener { - expandMessage() - } - tvLikes.setOnClickListener { chatMessage?.let { likeMessageEvents.onNext(it) } } - messageText.setOnClickListener { expandMessage() } - messageText.movementMethod = LinkMovementMethod.getInstance() - userLabel.setOnClickListener { chatMessage?.uuid?.let {userLabelClickEvents.onNext(it) } } - avatarView.setOnClickListener { chatMessage?.uuid?.let {userLabelClickEvents.onNext(it) } } - replyButton.setOnClickListener { - if (chatMessage?.username != null) { - chatMessage?.username?.let { replyMessageEvents.onNext(it) } - } else { - chatMessage?.user?.let { replyMessageEvents.onNext(it) } - } - } - replyButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatReplyIcon()), - null, null, null) - copyButton.setOnClickListener { chatMessage?.let { copyMessageEvents.onNext(it) } } - copyButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatCopyIcon()), - null, null, null) - reportButton.setOnClickListener { chatMessage?.let { flagMessageEvents.onNext(it) } } - reportButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatReportIcon()), - null, null, null) - deleteButton.setOnClickListener { chatMessage?.let { deleteMessageEvents.onNext(it) } } - deleteButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatDeleteIcon()), - null, null, null) - } - - fun bind(msg: ChatMessage, uuid: String) { - chatMessage = msg - userId = uuid - - setLikeProperties() - - val wasSent = messageWasSent() - - val name = user?.profile?.name - if (wasSent) { - userLabel.tier = user?.contributor?.level ?: 0 - userLabel.username = name - if (user?.username != null) { - @SuppressLint("SetTextI18n") - sublineTextView.text = "${user?.formattedUsername} ∙ ${msg.getAgoString(res)}" - } else { - sublineTextView.text = msg.getAgoString(res) - } - } else { - userLabel.tier = msg.contributor?.level ?: 0 - userLabel.username = msg.user - if (msg.username != null) { - @SuppressLint("SetTextI18n") - sublineTextView.text = "${msg.formattedUsername} ∙ ${msg.getAgoString(res)}" - } else { - sublineTextView.text = msg.getAgoString(res) - } - } - when { - userLabel.tier == 8 -> { - modView.visibility = View.VISIBLE - modView.text = context.getString(R.string.moderator) - modView.background = ContextCompat.getDrawable(context, R.drawable.pill_bg_blue) - modView.setScaledPadding(context, 12, 4, 12, 4) - } - userLabel.tier == 9 -> { - modView.visibility = View.VISIBLE - modView.text = context.getString(R.string.staff) - modView.background = ContextCompat.getDrawable(context, R.drawable.pill_bg_purple_300) - modView.setScaledPadding(context, 12, 4, 12, 4) - } - else -> modView.visibility = View.GONE - } - - if (wasSent) { - avatarView.visibility = View.GONE - itemView.setPadding(64.dpToPx(context), itemView.paddingTop, itemView.paddingRight, itemView.paddingBottom) - } else { - val displayMetrics = res.displayMetrics - val dpWidth = displayMetrics.widthPixels / displayMetrics.density - if (dpWidth > 350) { - avatarView.visibility = View.VISIBLE - msg.userStyles?.let { - avatarView.setAvatar(it) - } - } else { - avatarView.visibility = View.GONE - } - itemView.setPadding(16.dpToPx(context), itemView.paddingTop, itemView.paddingRight, itemView.paddingBottom) - } - - messageText.text = chatMessage?.parsedText - if (msg.parsedText == null) { - messageText.text = chatMessage?.text - Maybe.just(chatMessage?.text ?: "") - .map { MarkdownParser.parseMarkdown(it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ parsedText -> - chatMessage?.parsedText = parsedText - messageText.text = chatMessage?.parsedText - }, { it.printStackTrace() }) - } - - val username = user?.formattedUsername - messageWrapper.background = if ((name != null && msg.text?.contains("@$name") == true) || (username != null && msg.text?.contains(username) == true)) { - ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_brand_700) - } else { - ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg) - } - messageWrapper.setScaledPadding(context, 8, 8, 8, 8) - - if (expandedMessageId == msg.id) { - buttonsWrapper.visibility = View.VISIBLE - deleteButton.visibility = if (shouldShowDelete()) View.VISIBLE else View.GONE - replyButton.visibility = if (chatMessage?.isInboxMessage == true) View.GONE else View.VISIBLE - } else { - buttonsWrapper.visibility = View.GONE - } - } - - private fun messageWasSent(): Boolean { - return chatMessage?.sent == true || chatMessage?.uuid == userId - } - - private fun setLikeProperties() { - likeBackground.visibility = if (isTavern) View.VISIBLE else View.INVISIBLE - @SuppressLint("SetTextI18n") - tvLikes.text = "+" + chatMessage?.likeCount - - val backgroundColorRes: Int - val foregroundColorRes: Int - - if (chatMessage?.likeCount != 0) { - if (chatMessage?.userLikesMessage(userId) == true) { - backgroundColorRes = R.color.tavern_userliked_background - foregroundColorRes = R.color.tavern_userliked_foreground - } else { - backgroundColorRes = R.color.tavern_somelikes_background - foregroundColorRes = R.color.tavern_somelikes_foreground - } - } else { - backgroundColorRes = R.color.tavern_nolikes_background - foregroundColorRes = R.color.tavern_nolikes_foreground - } - - DataBindingUtils.setRoundedBackground(likeBackground, ContextCompat.getColor(context, backgroundColorRes)) - tvLikes.setTextColor(ContextCompat.getColor(context, foregroundColorRes)) - } - - private fun shouldShowDelete(): Boolean { - return chatMessage?.isSystemMessage != true && (chatMessage?.uuid == userId || user?.contributor?.admin == true || chatMessage?.isInboxMessage == true) - } - - private fun expandMessage() { - expandedMessageId = if (expandedMessageId == chatMessage?.id) { - null - } else { - chatMessage?.id - } - notifyItemChanged(adapterPosition) + private fun expandMessage(id: String, position: Int) { + expandedMessageId = if (expandedMessageId == id) { + null + } else { + id } + notifyItemChanged(position) } } + +class SystemChatMessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val textView: TextView by bindView(R.id.text_view) + + fun bind(chatMessage: ChatMessage?) { + textView.text = chatMessage?.text?.removePrefix("`")?.removeSuffix("`") + } + +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt new file mode 100644 index 000000000..c14967dad --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt @@ -0,0 +1,90 @@ +package com.habitrpg.android.habitica.ui.adapter.social + +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.inflate +import com.habitrpg.android.habitica.models.social.ChatMessage +import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerViewHolder +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.subjects.PublishSubject + +class InboxAdapter(private var user: User?) : PagedListAdapter(DIFF_CALLBACK) { + private var expandedMessageId: String? = null + + private val likeMessageEvents = PublishSubject.create() + private val userLabelClickEvents = PublishSubject.create() + private val deleteMessageEvents = PublishSubject.create() + private val flagMessageEvents = PublishSubject.create() + private val replyMessageEvents = PublishSubject.create() + private val copyMessageEvents = PublishSubject.create() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRecyclerViewHolder { + return ChatRecyclerViewHolder(parent.inflate(R.layout.tavern_chat_item), user?.id ?: "", false) + } + + override fun onBindViewHolder(holder: ChatRecyclerViewHolder, position: Int) { + val message = getItem(position) ?: return + + holder.bind(message, + user?.id ?: "", + user, + expandedMessageId == message.id) + holder.onShouldExpand = { expandMessage(message.id, position) } + holder.onLikeMessage = { likeMessageEvents.onNext(it) } + holder.onOpenProfile = { userLabelClickEvents.onNext(it) } + holder.onReply = { replyMessageEvents.onNext(it) } + holder.onCopyMessage = { copyMessageEvents.onNext(it) } + holder.onFlagMessage = { flagMessageEvents.onNext(it) } + holder.onDeleteMessage = { deleteMessageEvents.onNext(it) } + } + + fun getLikeMessageFlowable(): Flowable { + return likeMessageEvents.toFlowable(BackpressureStrategy.DROP) + } + + fun getUserLabelClickFlowable(): Flowable { + return userLabelClickEvents.toFlowable(BackpressureStrategy.DROP) + } + + fun getFlagMessageClickFlowable(): Flowable { + return flagMessageEvents.toFlowable(BackpressureStrategy.DROP) + } + + fun getDeleteMessageFlowable(): Flowable { + return deleteMessageEvents.toFlowable(BackpressureStrategy.DROP) + } + + fun getReplyMessageEvents(): Flowable { + return replyMessageEvents.toFlowable(BackpressureStrategy.DROP) + } + + fun getCopyMessageFlowable(): Flowable { + return copyMessageEvents.toFlowable(BackpressureStrategy.DROP) + } + + private fun expandMessage(id: String, position: Int) { + expandedMessageId = if (expandedMessageId == id) { + null + } else { + id + } + notifyItemChanged(position) + } + + companion object { + private val DIFF_CALLBACK = object : + DiffUtil.ItemCallback() { + // Concert details may have changed if reloaded from the database, + // but ID is fixed. + override fun areItemsTheSame(oldConcert: ChatMessage, + newConcert: ChatMessage) = oldConcert.id == newConcert.id + + override fun areContentsTheSame(oldConcert: ChatMessage, + newConcert: ChatMessage) = oldConcert == newConcert + } + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt index 735e497d0..a494408e6 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt @@ -8,6 +8,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import com.habitrpg.android.habitica.MainNavDirections import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.components.UserComponent @@ -18,10 +20,12 @@ import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.ui.activities.FullProfileActivity import com.habitrpg.android.habitica.ui.activities.MainActivity -import com.habitrpg.android.habitica.ui.adapter.social.ChatRecyclerViewAdapter +import com.habitrpg.android.habitica.ui.adapter.social.InboxAdapter import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment import com.habitrpg.android.habitica.ui.helpers.KeyboardUtil import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator +import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModel +import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModelFactory import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar.Companion.showSnackbar import io.reactivex.android.schedulers.AndroidSchedulers @@ -39,10 +43,12 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout @Inject lateinit var configManager: AppConfigManager - private var chatAdapter: ChatRecyclerViewAdapter? = null + private var chatAdapter: InboxAdapter? = null private var chatRoomUser: String? = null private var replyToUserUUID: String? = null + private var viewModel: InboxViewModel? = null + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { this.hidesToolbar = true @@ -59,11 +65,13 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout val args = InboxMessageListFragmentArgs.fromBundle(it) setReceivingUser(args.username, args.userID) } + viewModel = ViewModelProviders.of(this, InboxViewModelFactory(replyToUserUUID ?: "")).get(InboxViewModel::class.java) val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.getActivity()) recyclerView.layoutManager = layoutManager - chatAdapter = ChatRecyclerViewAdapter(null, true, user, false) + chatAdapter = InboxAdapter(user) + viewModel?.messages?.observe(this, Observer { chatAdapter?.submitList(it) }) recyclerView.adapter = chatAdapter recyclerView.itemAnimator = SafeDefaultItemAnimator() chatAdapter?.let { adapter -> @@ -78,8 +86,6 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout chatBarView.sendAction = { sendMessage(it) } chatBarView.maxChatLength = configManager.maxChatLength() - loadMessages() - communityGuidelinesView.visibility = View.GONE } @@ -90,14 +96,6 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout super.onAttach(context) } - private fun loadMessages() { - if (user?.isManaged == true) { - compositeSubscription.add(socialRepository.getInboxMessages(replyToUserUUID) - .firstElement() - .subscribe(Consumer { this.chatAdapter?.updateData(it) }, RxErrorHandler.handleEmptyError())) - } - } - override fun onDestroy() { socialRepository.close() super.onDestroy() @@ -107,16 +105,17 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout component.inject(this) } - private fun refreshUserInbox() { - this.swipeRefreshLayout?.isRefreshing = true - compositeSubscription.add(this.socialRepository.retrieveInboxMessages() + private fun refreshConversation() { + compositeSubscription.add(this.socialRepository.retrieveInboxMessages(replyToUserUUID ?: "", 0) .subscribe(Consumer {}, RxErrorHandler.handleEmptyError(), Action { swipeRefreshLayout?.isRefreshing = false + viewModel?.invalidateDataSource() })) } override fun onRefresh() { - this.refreshUserInbox() + this.swipeRefreshLayout?.isRefreshing = true + this.refreshConversation() } private fun sendMessage(chatText: String) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxOverviewFragment.kt similarity index 83% rename from Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxFragment.kt rename to Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxOverviewFragment.kt index 7fb005941..9128fbe61 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxOverviewFragment.kt @@ -11,10 +11,11 @@ import android.widget.TextView import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.SocialRepository +import com.habitrpg.android.habitica.extensions.getAgoString 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.social.ChatMessage +import com.habitrpg.android.habitica.models.social.InboxConversation import com.habitrpg.android.habitica.modules.AppModule import com.habitrpg.android.habitica.ui.AvatarView import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment @@ -27,7 +28,7 @@ import kotlinx.android.synthetic.main.fragment_inbox.* import javax.inject.Inject import javax.inject.Named -class InboxFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener, View.OnClickListener { +class InboxOverviewFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener, View.OnClickListener { @Inject lateinit var socialRepository: SocialRepository @@ -54,10 +55,11 @@ class InboxFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.Swi inbox_refresh_layout?.setOnRefreshListener(this) loadMessages() + retrieveMessages() } private fun loadMessages() { - compositeSubscription.add(socialRepository.getInboxOverviewList().subscribe(Consumer> { + compositeSubscription.add(socialRepository.getInboxConversations().subscribe(Consumer> { setInboxMessages(it) }, RxErrorHandler.handleEmptyError())) } @@ -68,9 +70,7 @@ class InboxFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.Swi } override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - - when (id) { + when (item.itemId) { R.id.send_message -> { openNewMessageDialog() return true @@ -103,15 +103,20 @@ class InboxFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.Swi component.inject(this) } - override fun onRefresh() { - inbox_refresh_layout.isRefreshing = true - compositeSubscription.add(this.socialRepository.retrieveInboxMessages() - .subscribe(Consumer> { + + private fun retrieveMessages() { + compositeSubscription.add(this.socialRepository.retrieveInboxConversations() + .subscribe(Consumer> { inbox_refresh_layout.isRefreshing = false }, RxErrorHandler.handleEmptyError())) } - private fun setInboxMessages(messages: RealmResults) { + override fun onRefresh() { + inbox_refresh_layout.isRefreshing = true + retrieveMessages() + } + + private fun setInboxMessages(messages: RealmResults) { if (inbox_messages == null) { return } @@ -123,15 +128,15 @@ class InboxFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.Swi for (message in messages) { val entry = inflater?.inflate(R.layout.item_inbox_overview, inbox_messages, false) val avatarView = entry?.findViewById(R.id.avatar_view) as? AvatarView - //message.userStyles?.let { avatarView?.setAvatar(it) } + message.userStyles?.let { avatarView?.setAvatar(it) } avatarView?.visibility = View.GONE val displayNameTextView = entry?.findViewById(R.id.display_name_textview) as? UsernameLabel displayNameTextView?.username = message.user displayNameTextView?.tier = message.contributor?.level ?: 0 val timestampTextView = entry?.findViewById(R.id.timestamp_textview) as? TextView - timestampTextView?.text = message.getAgoString(resources) + timestampTextView?.text = message.timestamp?.getAgoString(resources) val usernameTextView = entry?.findViewById(R.id.username_textview) as? TextView - if (message.username != null) { + if (message.username?.isNotEmpty() == true) { usernameTextView?.text = message.formattedUsername usernameTextView?.visibility = View.VISIBLE } else { @@ -156,7 +161,7 @@ class InboxFragment : BaseMainFragment(), androidx.swiperefreshlayout.widget.Swi } private fun openInboxMessages(userID: String, username: String) { - MainNavigationController.navigate(InboxFragmentDirections.openInboxDetail(userID, username)) + MainNavigationController.navigate(InboxOverviewFragmentDirections.openInboxDetail(userID, username)) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt new file mode 100644 index 000000000..2f29d743b --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt @@ -0,0 +1,214 @@ +package com.habitrpg.android.habitica.ui.viewHolders + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.graphics.drawable.BitmapDrawable +import android.text.method.LinkMovementMethod +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.extensions.getAgoString +import com.habitrpg.android.habitica.extensions.setScaledPadding +import com.habitrpg.android.habitica.models.social.ChatMessage +import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.ui.AvatarView +import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils +import com.habitrpg.android.habitica.ui.helpers.MarkdownParser +import com.habitrpg.android.habitica.ui.helpers.bindView +import com.habitrpg.android.habitica.ui.views.HabiticaEmojiTextView +import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper +import com.habitrpg.android.habitica.ui.views.social.UsernameLabel +import io.reactivex.Maybe +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class ChatRecyclerViewHolder(itemView: View, private var userId: String, private val isTavern: Boolean) : RecyclerView.ViewHolder(itemView) { + + private val messageWrapper: ViewGroup by bindView(R.id.message_wrapper) + private val avatarView: AvatarView by bindView(R.id.avatar_view) + private val userLabel: UsernameLabel by bindView(R.id.user_label) + private val messageText: HabiticaEmojiTextView by bindView(R.id.message_text) + private val sublineTextView: TextView by bindView(R.id.subline_textview) + private val likeBackground: LinearLayout by bindView(R.id.like_background_layout) + private val tvLikes: TextView by bindView(R.id.tvLikes) + private val buttonsWrapper: ViewGroup by bindView(R.id.buttons_wrapper) + private val replyButton: Button by bindView(R.id.reply_button) + private val copyButton: Button by bindView(R.id.copy_button) + private val reportButton: Button by bindView(R.id.report_button) + private val deleteButton: Button by bindView(R.id.delete_button) + private val modView: TextView by bindView(R.id.mod_view) + + val context: Context = itemView.context + val res: Resources = itemView.resources + private var chatMessage: ChatMessage? = null + private var user: User? = null + + var onShouldExpand: (() -> Unit)? = null + var onLikeMessage: ((ChatMessage) -> Unit)? = null + var onOpenProfile: ((String) -> Unit)? = null + var onReply: ((String) -> Unit)? = null + var onCopyMessage: ((ChatMessage) -> Unit)? = null + var onFlagMessage: ((ChatMessage) -> Unit)? = null + var onDeleteMessage: ((ChatMessage) -> Unit)? = null + + init { + itemView.setOnClickListener { + onShouldExpand?.invoke() + } + tvLikes.setOnClickListener { chatMessage?.let { onLikeMessage?.invoke(it) } } + messageText.setOnClickListener { onShouldExpand?.invoke() } + messageText.movementMethod = LinkMovementMethod.getInstance() + userLabel.setOnClickListener { chatMessage?.uuid?.let { onOpenProfile?.invoke(it) } } + avatarView.setOnClickListener { chatMessage?.uuid?.let { onOpenProfile?.invoke(it) } } + replyButton.setOnClickListener { + if (chatMessage?.username != null) { + chatMessage?.username?.let { onReply?.invoke(it) } + } else { + chatMessage?.user?.let { onReply?.invoke(it) } + } + } + replyButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatReplyIcon()), + null, null, null) + copyButton.setOnClickListener { chatMessage?.let { onCopyMessage?.invoke(it) } } + copyButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatCopyIcon()), + null, null, null) + reportButton.setOnClickListener { chatMessage?.let { onFlagMessage?.invoke(it) } } + reportButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatReportIcon()), + null, null, null) + deleteButton.setOnClickListener { chatMessage?.let { onDeleteMessage?.invoke(it) } } + deleteButton.setCompoundDrawablesWithIntrinsicBounds(BitmapDrawable(res, HabiticaIconsHelper.imageOfChatDeleteIcon()), + null, null, null) + } + + fun bind(msg: ChatMessage, uuid: String, user: User?, isExpanded: Boolean) { + chatMessage = msg + this.user = user + userId = uuid + + setLikeProperties() + + val wasSent = messageWasSent() + + val name = user?.profile?.name + if (wasSent) { + userLabel.tier = user?.contributor?.level ?: 0 + userLabel.username = name + if (user?.username != null) { + @SuppressLint("SetTextI18n") + sublineTextView.text = "${user.formattedUsername} ∙ ${msg.timestamp?.getAgoString(res)}" + } else { + sublineTextView.text = msg.timestamp?.getAgoString(res) + } + } else { + userLabel.tier = msg.contributor?.level ?: 0 + userLabel.username = msg.user + if (msg.username != null) { + @SuppressLint("SetTextI18n") + sublineTextView.text = "${msg.formattedUsername} ∙ ${msg.timestamp?.getAgoString(res)}" + } else { + sublineTextView.text = msg.timestamp?.getAgoString(res) + } + } + when { + userLabel.tier == 8 -> { + modView.visibility = View.VISIBLE + modView.text = context.getString(R.string.moderator) + modView.background = ContextCompat.getDrawable(context, R.drawable.pill_bg_blue) + modView.setScaledPadding(context, 12, 4, 12, 4) + } + userLabel.tier == 9 -> { + modView.visibility = View.VISIBLE + modView.text = context.getString(R.string.staff) + modView.background = ContextCompat.getDrawable(context, R.drawable.pill_bg_purple_300) + modView.setScaledPadding(context, 12, 4, 12, 4) + } + else -> modView.visibility = View.GONE + } + + if (wasSent) { + avatarView.visibility = View.GONE + itemView.setPadding(64.dpToPx(context), itemView.paddingTop, itemView.paddingRight, itemView.paddingBottom) + } else { + val displayMetrics = res.displayMetrics + val dpWidth = displayMetrics.widthPixels / displayMetrics.density + if (dpWidth > 350) { + avatarView.visibility = View.VISIBLE + msg.userStyles?.let { + avatarView.setAvatar(it) + } + } else { + avatarView.visibility = View.GONE + } + itemView.setPadding(16.dpToPx(context), itemView.paddingTop, itemView.paddingRight, itemView.paddingBottom) + } + + messageText.text = chatMessage?.parsedText + if (msg.parsedText == null) { + messageText.text = chatMessage?.text + Maybe.just(chatMessage?.text ?: "") + .map { MarkdownParser.parseMarkdown(it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ parsedText -> + chatMessage?.parsedText = parsedText + messageText.text = chatMessage?.parsedText + }, { it.printStackTrace() }) + } + + val username = user?.formattedUsername + messageWrapper.background = if ((name != null && msg.text?.contains("@$name") == true) || (username != null && msg.text?.contains(username) == true)) { + ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_brand_700) + } else { + ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg) + } + messageWrapper.setScaledPadding(context, 8, 8, 8, 8) + + if (isExpanded) { + buttonsWrapper.visibility = View.VISIBLE + deleteButton.visibility = if (shouldShowDelete()) View.VISIBLE else View.GONE + replyButton.visibility = if (chatMessage?.isInboxMessage == true) View.GONE else View.VISIBLE + } else { + buttonsWrapper.visibility = View.GONE + } + } + + private fun messageWasSent(): Boolean { + return chatMessage?.sent == true || chatMessage?.uuid == userId + } + + private fun setLikeProperties() { + likeBackground.visibility = if (isTavern) View.VISIBLE else View.INVISIBLE + @SuppressLint("SetTextI18n") + tvLikes.text = "+" + chatMessage?.likeCount + + val backgroundColorRes: Int + val foregroundColorRes: Int + + if (chatMessage?.likeCount != 0) { + if (chatMessage?.userLikesMessage(userId) == true) { + backgroundColorRes = R.color.tavern_userliked_background + foregroundColorRes = R.color.tavern_userliked_foreground + } else { + backgroundColorRes = R.color.tavern_somelikes_background + foregroundColorRes = R.color.tavern_somelikes_foreground + } + } else { + backgroundColorRes = R.color.tavern_nolikes_background + foregroundColorRes = R.color.tavern_nolikes_foreground + } + + DataBindingUtils.setRoundedBackground(likeBackground, ContextCompat.getColor(context, backgroundColorRes)) + tvLikes.setTextColor(ContextCompat.getColor(context, foregroundColorRes)) + } + + private fun shouldShowDelete(): Boolean { + return chatMessage?.isSystemMessage != true && (chatMessage?.uuid == userId || user?.contributor?.admin == true || chatMessage?.isInboxMessage == true) + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt new file mode 100644 index 000000000..327cccb93 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt @@ -0,0 +1,100 @@ +package com.habitrpg.android.habitica.ui.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.paging.DataSource +import androidx.paging.PagedList +import androidx.paging.PositionalDataSource +import androidx.paging.toLiveData +import com.habitrpg.android.habitica.components.UserComponent +import com.habitrpg.android.habitica.data.SocialRepository +import com.habitrpg.android.habitica.helpers.RxErrorHandler +import com.habitrpg.android.habitica.models.social.ChatMessage +import io.reactivex.Flowable +import io.reactivex.functions.Consumer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.math.ceil + + +class InboxViewModel(recipientID: String) : BaseViewModel() { + @Inject + lateinit var socialRepository: SocialRepository + + private val config = PagedList.Config.Builder() + .setPageSize(10) + .setEnablePlaceholders(false) + .build() + + private val dataSourceFactory = MessagesDataSourceFactory(socialRepository, recipientID) + val messages: LiveData> = dataSourceFactory.toLiveData(config) + + override fun inject(component: UserComponent) { + component.inject(this) + } + + fun invalidateDataSource() { + dataSourceFactory.sourceLiveData.value?.invalidate() + } +} + +private class MessagesDataSource(val socialRepository: SocialRepository, val recipientID: String): + PositionalDataSource() { + private var lastFetchWasEnd = false + override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { + if (lastFetchWasEnd) { + callback.onResult(emptyList()) + return + } + GlobalScope.launch(Dispatchers.Main.immediate) { + val page = ceil(params.startPosition.toFloat() / params.loadSize.toFloat()).toInt() + socialRepository.retrieveInboxMessages(recipientID, page) + .subscribe(Consumer { + if (it.size != 10) lastFetchWasEnd = true + callback.onResult(it) + }, RxErrorHandler.handleEmptyError()) + } + } + + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + lastFetchWasEnd = false + GlobalScope.launch(Dispatchers.Main.immediate) { + socialRepository.getInboxMessages(recipientID).firstElement() + .flatMapPublisher { + if (it.size == 0) { + socialRepository.retrieveInboxMessages(recipientID, 0) + .doOnNext { + messages -> if (messages.size != 10) lastFetchWasEnd = true + } + } else { + Flowable.just(it) + } + } + .subscribe(Consumer { + callback.onResult(it, 0) + }, RxErrorHandler.handleEmptyError()) + } + } +} + +private class MessagesDataSourceFactory(val socialRepository: SocialRepository, val recipientID: String) : + DataSource.Factory() { + val sourceLiveData = MutableLiveData() + var latestSource: MessagesDataSource = MessagesDataSource(socialRepository, recipientID) + override fun create(): DataSource { + latestSource = MessagesDataSource(socialRepository, recipientID) + sourceLiveData.postValue(latestSource) + return latestSource + } +} + +class InboxViewModelFactory(private val recipientID: String) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return InboxViewModel(recipientID) as T + } +} \ No newline at end of file