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