Implement pagination in inbox. Fixes #1205

This commit is contained in:
Phillip Thelen 2019-08-25 13:59:11 +02:00
parent a9b1a54dbc
commit 5e6f4fe45b
20 changed files with 604 additions and 321 deletions

View file

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

View file

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="FrameLayout">
android:layout_height="wrap_content">
<com.habitrpg.android.habitica.ui.views.NPCBannerView
android:id="@+id/npcBannerView"
@ -39,4 +38,4 @@
android:layout_marginBottom="4dp"
style="@style/Body1"
tools:text="Welcome to the Market! Stock up on new gear or buy rare eggs and potions. Check in periodically for new stock." />
</merge>
</FrameLayout>

View file

@ -154,7 +154,7 @@
android:label="@string/sidebar_about" />
<fragment
android:id="@+id/inboxFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.social.InboxFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.social.InboxOverviewFragment"
android:label="@string/sidebar_inbox" >
<action
android:id="@+id/openInboxDetail"

View file

@ -10,10 +10,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
@ -33,8 +30,10 @@ interface ApiService {
val user: Flowable<HabitResponse<User>>
@get:GET("inbox/messages")
val inboxMessages: Flowable<HabitResponse<List<ChatMessage>>>
@GET("inbox/messages")
fun getInboxMessages(@Query("conversation") uuid: String, @Query("page") page: Int): Flowable<HabitResponse<List<ChatMessage>>>
@GET("inbox/conversations")
fun getInboxConversations(): Flowable<HabitResponse<List<InboxConversation>>>
@get:GET("tasks/user")

View file

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

View file

@ -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<User>
fun retrieveInboxMessages(): Flowable<List<ChatMessage>>
fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>>
fun retrieveInboxConversations(): Flowable<List<InboxConversation>>
fun <T> configureApiCallObserver(): FlowableTransformer<HabitResponse<T>, T>

View file

@ -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<RealmResults<ChatMessage>>
fun retrieveInboxMessages(): Flowable<List<ChatMessage>>
fun getInboxOverviewList(): Flowable<RealmResults<ChatMessage>>
fun postPrivateMessage(messageObject: HashMap<String, String>): Flowable<List<ChatMessage>>
fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>>
fun retrieveInboxConversations(): Flowable<List<InboxConversation>>
fun getInboxConversations(): Flowable<RealmResults<InboxConversation>>
fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): Flowable<List<ChatMessage>>
fun postPrivateMessage(recipientId: String, message: String): Flowable<List<ChatMessage>>

View file

@ -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<List<ChatMessage>> {
return apiService.inboxMessages.compose(configureApiCallObserver())
override fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>> {
return apiService.getInboxMessages(uuid, page).compose(configureApiCallObserver())
}
override fun retrieveInboxConversations(): Flowable<List<InboxConversation>> {
return apiService.getInboxConversations().compose(configureApiCallObserver())
}
override fun hasAuthenticationKeys(): Boolean {

View file

@ -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<RealmResults<Group>> = localRepository.getPublicGuilds()
override fun getInboxOverviewList(): Flowable<RealmResults<ChatMessage>> = localRepository.getInboxOverviewList(userID)
override fun getInboxConversations(): Flowable<RealmResults<InboxConversation>> = localRepository.getInboxConversation(userID)
override fun getInboxMessages(replyToUserID: String?): Flowable<RealmResults<ChatMessage>> = localRepository.getInboxMessages(userID, replyToUserID)
override fun retrieveInboxMessages(): Flowable<List<ChatMessage>> {
return apiClient.retrieveInboxMessages().doOnNext { messages ->
override fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>> {
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<String, String>): Flowable<List<ChatMessage>> {
return apiClient.postPrivateMessage(messageObject).flatMap { retrieveInboxMessages() }
override fun retrieveInboxConversations(): Flowable<List<InboxConversation>> {
return apiClient.retrieveInboxConversations().doOnNext { conversations ->
localRepository.saveInboxConversations(userID, conversations)
}
}
override fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): Flowable<List<ChatMessage>> {
return apiClient.postPrivateMessage(messageObject).flatMap { retrieveInboxMessages(recipientId, 0) }
}
override fun postPrivateMessage(recipientId: String, message: String): Flowable<List<ChatMessage>> {
val messageObject = HashMap<String, String>()
messageObject["message"] = message
messageObject["toUserId"] = recipientId
return postPrivateMessage(messageObject)
return postPrivateMessage(recipientId, messageObject)
}
override fun getGroupMembers(id: String): Flowable<RealmResults<Member>> = localRepository.getGroupMembers(id)

View file

@ -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<RealmResults<ChatMessage>>
fun getInboxOverviewList(userId: String): Flowable<RealmResults<ChatMessage>>
fun getInboxConversation(userId: String): Flowable<RealmResults<InboxConversation>>
fun saveGroupMemberships(userID: String?, memberships: List<GroupMembership>)
fun saveInboxMessages(userID: String, messages: List<ChatMessage>)
fun saveInboxMessages(userID: String, recipientID: String, messages: List<ChatMessage>, page: Int)
fun saveInboxConversations(userID: String, conversations: List<InboxConversation>)
fun getChatMessage(messageID: String): Flowable<ChatMessage>
}

View file

@ -49,9 +49,11 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
}
}
override fun saveInboxMessages(userID: String, messages: List<ChatMessage>) {
override fun saveInboxMessages(userID: String, recipientID: String, messages: List<ChatMessage>, 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<ChatMessage>()
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<InboxConversation>) {
conversations.forEach { it.userID = userID }
realm.executeTransaction { realm.insertOrUpdate(conversations) }
val existingConversations = realm.where(InboxConversation::class.java).findAll()
val conversationsToRemove = ArrayList<InboxConversation>()
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<GroupMembership>) {
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<RealmResults<ChatMessage>> {
override fun getInboxMessages(userID: String, replyToUserID: String?): Flowable<RealmResults<ChatMessage>> {
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<RealmResults<ChatMessage>> {
return realm.where(ChatMessage::class.java)
.equalTo("isInboxMessage", true)
override fun getInboxConversation(userID: String): Flowable<RealmResults<InboxConversation>> {
return realm.where(InboxConversation::class.java)
.equalTo("userID", userID)
.sort("timestamp", Sort.DESCENDING)
.distinct("uuid")
.findAll()
.asFlowable()
.filter { it.isLoaded }

View file

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

View file

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

View file

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

View file

@ -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<ChatMessage>?, 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<ChatMessage>?, 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("`")
}
}

View file

@ -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<ChatMessage, ChatRecyclerViewHolder>(DIFF_CALLBACK) {
private var expandedMessageId: String? = null
private val likeMessageEvents = PublishSubject.create<ChatMessage>()
private val userLabelClickEvents = PublishSubject.create<String>()
private val deleteMessageEvents = PublishSubject.create<ChatMessage>()
private val flagMessageEvents = PublishSubject.create<ChatMessage>()
private val replyMessageEvents = PublishSubject.create<String>()
private val copyMessageEvents = PublishSubject.create<ChatMessage>()
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<ChatMessage> {
return likeMessageEvents.toFlowable(BackpressureStrategy.DROP)
}
fun getUserLabelClickFlowable(): Flowable<String> {
return userLabelClickEvents.toFlowable(BackpressureStrategy.DROP)
}
fun getFlagMessageClickFlowable(): Flowable<ChatMessage> {
return flagMessageEvents.toFlowable(BackpressureStrategy.DROP)
}
fun getDeleteMessageFlowable(): Flowable<ChatMessage> {
return deleteMessageEvents.toFlowable(BackpressureStrategy.DROP)
}
fun getReplyMessageEvents(): Flowable<String> {
return replyMessageEvents.toFlowable(BackpressureStrategy.DROP)
}
fun getCopyMessageFlowable(): Flowable<ChatMessage> {
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<ChatMessage>() {
// 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
}
}
}

View file

@ -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) {

View file

@ -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<RealmResults<ChatMessage>> {
compositeSubscription.add(socialRepository.getInboxConversations().subscribe(Consumer<RealmResults<InboxConversation>> {
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<List<ChatMessage>> {
private fun retrieveMessages() {
compositeSubscription.add(this.socialRepository.retrieveInboxConversations()
.subscribe(Consumer<List<InboxConversation>> {
inbox_refresh_layout.isRefreshing = false
}, RxErrorHandler.handleEmptyError()))
}
private fun setInboxMessages(messages: RealmResults<ChatMessage>) {
override fun onRefresh() {
inbox_refresh_layout.isRefreshing = true
retrieveMessages()
}
private fun setInboxMessages(messages: RealmResults<InboxConversation>) {
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))
}
}

View file

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

View file

@ -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<PagedList<ChatMessage>> = 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<ChatMessage>() {
private var lastFetchWasEnd = false
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<ChatMessage>) {
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<ChatMessage>) {
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<Int, ChatMessage>() {
val sourceLiveData = MutableLiveData<MessagesDataSource>()
var latestSource: MessagesDataSource = MessagesDataSource(socialRepository, recipientID)
override fun create(): DataSource<Int, ChatMessage> {
latestSource = MessagesDataSource(socialRepository, recipientID)
sourceLiveData.postValue(latestSource)
return latestSource
}
}
class InboxViewModelFactory(private val recipientID: String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return InboxViewModel(recipientID) as T
}
}