Implement LOCAL thread notifications that work only on real time.
This commit is contained in:
parent
d1bb96cec0
commit
c40a686cff
|
@ -106,6 +106,13 @@ class FlowRoom(private val room: Room) {
|
||||||
room.getAllThreads()
|
room.getAllThreads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveLocalUnreadThreadList(): Flow<List<TimelineEvent>> {
|
||||||
|
return room.getNumberOfLocalThreadNotificationsLive().asFlow()
|
||||||
|
.startWith(room.coroutineDispatchers.io) {
|
||||||
|
room.getNumberOfLocalThreadNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.flow(): FlowRoom {
|
fun Room.flow(): FlowRoom {
|
||||||
|
|
|
@ -68,11 +68,28 @@ interface TimelineService {
|
||||||
*/
|
*/
|
||||||
fun getAllThreads(): List<TimelineEvent>
|
fun getAllThreads(): List<TimelineEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a live list of all the local unread threads for the specified roomId
|
||||||
|
* @return the [LiveData] of [TimelineEvent]
|
||||||
|
*/
|
||||||
|
fun getNumberOfLocalThreadNotificationsLive(): LiveData<List<TimelineEvent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all the local unread threads for the specified roomId
|
||||||
|
* @return the [LiveData] of [TimelineEvent]
|
||||||
|
*/
|
||||||
|
fun getNumberOfLocalThreadNotifications(): List<TimelineEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether or not the current user is participating in the thread
|
* Returns whether or not the current user is participating in the thread
|
||||||
* @param rootThreadEventId the eventId of the current thread
|
* @param rootThreadEventId the eventId of the current thread
|
||||||
*/
|
*/
|
||||||
fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean
|
fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current thread as read. This is a local implementation
|
||||||
|
* @param rootThreadEventId the eventId of the current thread
|
||||||
|
*/
|
||||||
|
suspend fun markThreadAsRead(rootThreadEventId: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,5 +22,6 @@ data class ThreadDetails(
|
||||||
val isRootThread: Boolean = false,
|
val isRootThread: Boolean = false,
|
||||||
val numberOfThreads: Int = 0,
|
val numberOfThreads: Int = 0,
|
||||||
val threadSummarySenderInfo: SenderInfo? = null,
|
val threadSummarySenderInfo: SenderInfo? = null,
|
||||||
val threadSummaryLatestTextMessage: String? = null
|
val threadSummaryLatestTextMessage: String? = null,
|
||||||
|
val hasUnreadMessage: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
|
@ -375,6 +375,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
|
||||||
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||||
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||||
?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
|
?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
|
||||||
|
?.addField(EventEntityFields.HAS_UNREAD_THREAD_MESSAGES, Boolean::class.java)
|
||||||
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
|
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||||
* Finds the root thread event and update it with the latest message summary along with the number
|
* Finds the root thread event and update it with the latest message summary along with the number
|
||||||
* of threads included. If there is no root thread event no action is done
|
* of threads included. If there is no root thread event no action is done
|
||||||
*/
|
*/
|
||||||
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded() {
|
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(isInitialSync: Boolean = false, currentUserId: String? = null) {
|
||||||
|
|
||||||
if (!BuildConfig.THREADING_ENABLED) return
|
if (!BuildConfig.THREADING_ENABLED) return
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded() {
|
||||||
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
|
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
|
||||||
|
|
||||||
rootThreadEvent?.markEventAsRoot(
|
rootThreadEvent?.markEventAsRoot(
|
||||||
|
isInitialSync = isInitialSync,
|
||||||
|
currentUserId = currentUserId,
|
||||||
threadsCounted = it.size,
|
threadsCounted = it.size,
|
||||||
latestMessageTimelineEventEntity = latestMessage
|
latestMessageTimelineEventEntity = latestMessage
|
||||||
)
|
)
|
||||||
|
@ -68,11 +70,20 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? =
|
||||||
/**
|
/**
|
||||||
* Mark or update the current event a root thread event
|
* Mark or update the current event a root thread event
|
||||||
*/
|
*/
|
||||||
internal fun EventEntity.markEventAsRoot(threadsCounted: Int,
|
internal fun EventEntity.markEventAsRoot(
|
||||||
|
isInitialSync: Boolean,
|
||||||
|
currentUserId: String?,
|
||||||
|
threadsCounted: Int,
|
||||||
latestMessageTimelineEventEntity: TimelineEventEntity?) {
|
latestMessageTimelineEventEntity: TimelineEventEntity?) {
|
||||||
isRootThread = true
|
isRootThread = true
|
||||||
numberOfThreads = threadsCounted
|
numberOfThreads = threadsCounted
|
||||||
threadSummaryLatestMessage = latestMessageTimelineEventEntity
|
threadSummaryLatestMessage = latestMessageTimelineEventEntity
|
||||||
|
// skip notification coming from messages from the same user, also retain already marked events
|
||||||
|
hasUnreadThreadMessages = if (hasUnreadThreadMessages) {
|
||||||
|
latestMessageTimelineEventEntity?.root?.sender != currentUserId
|
||||||
|
} else {
|
||||||
|
if (latestMessageTimelineEventEntity?.root?.sender == currentUserId) false else !isInitialSync
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,6 +107,16 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
|
||||||
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
||||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the number of all the local notifications for the specified room
|
||||||
|
* @param roomId The room that the number of notifications will be returned
|
||||||
|
*/
|
||||||
|
internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
|
||||||
|
TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.HAS_UNREAD_THREAD_MESSAGES, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether or not the given user is participating in a current thread
|
* Returns whether or not the given user is participating in a current thread
|
||||||
* @param roomId the room that the thread exists
|
* @param roomId the room that the thread exists
|
||||||
|
|
|
@ -55,6 +55,7 @@ internal object EventMapper {
|
||||||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||||
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
||||||
|
eventEntity.hasUnreadThreadMessages = event.threadDetails?.hasUnreadMessage ?: false
|
||||||
eventEntity.rootThreadEventId = event.getRootThreadEventId()
|
eventEntity.rootThreadEventId = event.getRootThreadEventId()
|
||||||
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
|
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
|
||||||
return eventEntity
|
return eventEntity
|
||||||
|
@ -111,6 +112,7 @@ internal object EventMapper {
|
||||||
avatarUrl = timelineEventEntity.senderAvatar
|
avatarUrl = timelineEventEntity.senderAvatar
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
hasUnreadMessage = eventEntity.hasUnreadThreadMessages,
|
||||||
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
|
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||||
@Index var isRootThread: Boolean = false,
|
@Index var isRootThread: Boolean = false,
|
||||||
@Index var rootThreadEventId: String? = null,
|
@Index var rootThreadEventId: String? = null,
|
||||||
var numberOfThreads: Int = 0,
|
var numberOfThreads: Int = 0,
|
||||||
|
var hasUnreadThreadMessages: Boolean = false,
|
||||||
var threadSummaryLatestMessage: TimelineEventEntity? = null
|
var threadSummaryLatestMessage: TimelineEventEntity? = null
|
||||||
|
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
|
@ -32,9 +32,11 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
|
||||||
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
|
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
|
||||||
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
|
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
|
||||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
@ -42,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
|
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
|
||||||
internal class DefaultTimelineService @AssistedInject constructor(
|
internal class DefaultTimelineService @AssistedInject constructor(
|
||||||
@Assisted private val roomId: String,
|
@Assisted private val roomId: String,
|
||||||
|
@ -106,6 +109,20 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfLocalThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfLocalThreadNotifications(): List<TimelineEvent> {
|
||||||
|
return monarchy.fetchAllMappedSync(
|
||||||
|
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
|
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
|
||||||
return monarchy.findAllMappedWithChanges(
|
return monarchy.findAllMappedWithChanges(
|
||||||
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||||
|
@ -129,4 +146,12 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
||||||
senderId = senderId)
|
senderId = senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun markThreadAsRead(rootThreadEventId: String) {
|
||||||
|
monarchy.awaitTransaction {
|
||||||
|
EventEntity.where(
|
||||||
|
realm = it,
|
||||||
|
eventId = rootThreadEventId).findFirst()?.hasUnreadThreadMessages = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -267,7 +267,9 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
||||||
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded()
|
// passing isInitialSync = true because we want to disable local notifications
|
||||||
|
// they do not work properly without the API
|
||||||
|
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(true)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,7 +425,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded()
|
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(insertType == EventInsertType.INITIAL_SYNC, userId)
|
||||||
|
|
||||||
// posting new events to timeline if any is registered
|
// posting new events to timeline if any is registered
|
||||||
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
||||||
|
|
|
@ -189,8 +189,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
|
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
|
||||||
prepareForEncryption()
|
prepareForEncryption()
|
||||||
}
|
}
|
||||||
|
markThreadTimelineAsReadLocal()
|
||||||
|
observeLocalThreadNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun observeDataStore() {
|
private fun observeDataStore() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
vectorDataStore.pushCounterFlow.collect { nbOfPush ->
|
vectorDataStore.pushCounterFlow.collect { nbOfPush ->
|
||||||
|
@ -280,6 +284,17 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe local unread threads
|
||||||
|
*/
|
||||||
|
private fun observeLocalThreadNotifications(){
|
||||||
|
room.flow()
|
||||||
|
.liveLocalUnreadThreadList()
|
||||||
|
.execute {
|
||||||
|
copy(numberOfLocalUnreadThreads = it.invoke()?.size ?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
|
fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
|
||||||
|
|
||||||
fun getRoomSummary() = room.roomSummary()
|
fun getRoomSummary() = room.roomSummary()
|
||||||
|
@ -1112,6 +1127,17 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the thread as read, while the user navigated within the thread
|
||||||
|
* This is a local implementation has nothing to do with APIs
|
||||||
|
*/
|
||||||
|
private fun markThreadTimelineAsReadLocal(){
|
||||||
|
initialState.rootThreadEventId?.let{
|
||||||
|
session.coroutineScope.launch {
|
||||||
|
room.markThreadAsRead(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
|
||||||
timelineEvents.tryEmit(snapshot)
|
timelineEvents.tryEmit(snapshot)
|
||||||
|
|
|
@ -67,8 +67,9 @@ data class RoomDetailViewState(
|
||||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||||
val hasFailedSending: Boolean = false,
|
val hasFailedSending: Boolean = false,
|
||||||
val jitsiState: JitsiState = JitsiState(),
|
val jitsiState: JitsiState = JitsiState(),
|
||||||
val rootThreadEventId: String? = null
|
val rootThreadEventId: String? = null,
|
||||||
) : MavericksState {
|
val numberOfLocalUnreadThreads: Int = 0
|
||||||
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: TimelineArgs) : this(
|
constructor(args: TimelineArgs) : this(
|
||||||
roomId = args.roomId,
|
roomId = args.roomId,
|
||||||
|
|
|
@ -1031,9 +1031,9 @@ class TimelineFragment @Inject constructor(
|
||||||
val badgeFrameLayout = menuThreadList.findViewById<FrameLayout>(R.id.threadNotificationBadgeFrameLayout)
|
val badgeFrameLayout = menuThreadList.findViewById<FrameLayout>(R.id.threadNotificationBadgeFrameLayout)
|
||||||
val badgeTextView = menuThreadList.findViewById<TextView>(R.id.threadNotificationBadgeTextView)
|
val badgeTextView = menuThreadList.findViewById<TextView>(R.id.threadNotificationBadgeTextView)
|
||||||
|
|
||||||
val unreadThreadMessages = 18 + state.pushCounter
|
val unreadThreadMessages = state.numberOfLocalUnreadThreads
|
||||||
|
val userIsMentioned = false
|
||||||
|
|
||||||
val userIsMentioned = true
|
|
||||||
if (unreadThreadMessages > 0) {
|
if (unreadThreadMessages > 0) {
|
||||||
badgeFrameLayout.isVisible = true
|
badgeFrameLayout.isVisible = true
|
||||||
badgeTextView.text = unreadThreadMessages.toString()
|
badgeTextView.text = unreadThreadMessages.toString()
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.threads.list.model
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
@ -42,6 +43,7 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
||||||
@EpoxyAttribute lateinit var date: String
|
@EpoxyAttribute lateinit var date: String
|
||||||
@EpoxyAttribute lateinit var rootMessage: String
|
@EpoxyAttribute lateinit var rootMessage: String
|
||||||
@EpoxyAttribute lateinit var lastMessage: String
|
@EpoxyAttribute lateinit var lastMessage: String
|
||||||
|
@EpoxyAttribute var unreadMessage: Boolean = false
|
||||||
@EpoxyAttribute lateinit var lastMessageCounter: String
|
@EpoxyAttribute lateinit var lastMessageCounter: String
|
||||||
@EpoxyAttribute var rootMessageDeleted: Boolean = false
|
@EpoxyAttribute var rootMessageDeleted: Boolean = false
|
||||||
@EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null
|
@EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null
|
||||||
|
@ -69,6 +71,7 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
||||||
holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName()
|
holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName()
|
||||||
holder.lastMessageTextView.text = lastMessage
|
holder.lastMessageTextView.text = lastMessage
|
||||||
holder.lastMessageCounterTextView.text = lastMessageCounter
|
holder.lastMessageCounterTextView.text = lastMessageCounter
|
||||||
|
holder.unreadImageView.isVisible = unreadMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -79,6 +82,8 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
||||||
val lastMessageAvatarImageView by bind<ImageView>(R.id.messageThreadSummaryAvatarImageView)
|
val lastMessageAvatarImageView by bind<ImageView>(R.id.messageThreadSummaryAvatarImageView)
|
||||||
val lastMessageCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
|
val lastMessageCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
|
||||||
val lastMessageTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
|
val lastMessageTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
|
||||||
|
val unreadImageView by bind<ImageView>(R.id.threadSummaryUnreadImageView)
|
||||||
|
|
||||||
val rootView by bind<ConstraintLayout>(R.id.threadSummaryRootConstraintLayout)
|
val rootView by bind<ConstraintLayout>(R.id.threadSummaryRootConstraintLayout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ class ThreadListController @Inject constructor(
|
||||||
title(timelineEvent.senderInfo.displayName)
|
title(timelineEvent.senderInfo.displayName)
|
||||||
date(date)
|
date(date)
|
||||||
rootMessageDeleted(timelineEvent.root.isRedacted())
|
rootMessageDeleted(timelineEvent.root.isRedacted())
|
||||||
|
unreadMessage(timelineEvent.root.threadDetails?.hasUnreadMessage ?: false)
|
||||||
rootMessage(timelineEvent.root.getDecryptedTextSummary())
|
rootMessage(timelineEvent.root.getDecryptedTextSummary())
|
||||||
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
|
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
|
||||||
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
|
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/threadSummaryRootConstraintLayout"
|
android:id="@+id/threadSummaryRootConstraintLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="12dp"
|
|
||||||
android:paddingTop="12dp"
|
|
||||||
android:paddingEnd="0dp"
|
|
||||||
android:background="?android:colorBackground"
|
android:background="?android:colorBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:foreground="?attr/selectableItemBackground">
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingEnd="0dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/threadSummaryAvatarImageView"
|
android:id="@+id/threadSummaryAvatarImageView"
|
||||||
|
@ -32,8 +31,8 @@
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@color/element_name_04"
|
android:textColor="@color/element_name_04"
|
||||||
|
android:textStyle="bold"
|
||||||
app:layout_constraintEnd_toStartOf="@id/threadSummaryDateTextView"
|
app:layout_constraintEnd_toStartOf="@id/threadSummaryDateTextView"
|
||||||
app:layout_constraintStart_toEndOf="@id/threadSummaryAvatarImageView"
|
app:layout_constraintStart_toEndOf="@id/threadSummaryAvatarImageView"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
@ -47,14 +46,28 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginEnd="25dp"
|
android:layout_marginEnd="25dp"
|
||||||
android:maxLines="1"
|
|
||||||
android:gravity="end"
|
android:gravity="end"
|
||||||
|
android:maxLines="1"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/threadSummaryTitleTextView"
|
app:layout_constraintBottom_toBottomOf="@id/threadSummaryTitleTextView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/threadSummaryTitleTextView"
|
app:layout_constraintTop_toTopOf="@id/threadSummaryTitleTextView"
|
||||||
tools:text="10 minutes" />
|
tools:text="10 minutes" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/threadSummaryUnreadImageView"
|
||||||
|
android:layout_width="8dp"
|
||||||
|
android:layout_height="8dp"
|
||||||
|
android:src="@drawable/notification_badge"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/threadSummaryDateTextView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/threadSummaryDateTextView"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/threadSummaryDateTextView"
|
||||||
|
app:tint="@color/palette_gray_200"
|
||||||
|
tools:ignore="ContentDescription"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/threadSummaryRootMessageTextView"
|
android:id="@+id/threadSummaryRootMessageTextView"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
|
Loading…
Reference in New Issue