- Enhance local notification to work with read receipt & the latest chunk

- Local notification mentioning system
- Fix/Improve thread list filtering
This commit is contained in:
ariskotsomitopoulos 2021-12-14 13:35:08 +02:00
parent 5c015a7444
commit d56281dca7
20 changed files with 318 additions and 63 deletions

View file

@ -84,7 +84,7 @@ interface TimelineService {
* Returns whether or not the current user is participating in the thread
* @param rootThreadEventId the eventId of the current thread
*/
fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
/**
* Marks the current thread as read. This is a local implementation

View file

@ -18,10 +18,14 @@ package org.matrix.android.sdk.api.session.threads
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
/**
* This class contains all the details needed for threads.
* Is is mainly used from within an Event.
*/
data class ThreadDetails(
val isRootThread: Boolean = false,
val numberOfThreads: Int = 0,
val threadSummarySenderInfo: SenderInfo? = null,
val threadSummaryLatestTextMessage: String? = null,
val hasUnreadMessage: Boolean = false
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
)

View file

@ -0,0 +1,25 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
/**
* This class defines the state of a thread notification badge
*/
data class ThreadNotificationBadgeState(
val numberOfLocalUnreadThreads: Int = 0,
val isUserMentioned: Boolean = false
)

View file

@ -0,0 +1,34 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
/**
* This class defines the state of a thread notification
*/
enum class ThreadNotificationState {
// There are no new message
NO_NEW_MESSAGE,
// There is at least one new message
NEW_MESSAGE,
// The is at least one new message that should bi highlighted
// ex. "Hello @aris.kotsomitopoulos"
NEW_HIGHLIGHTED_MESSAGE;
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
* This class contains a thread TimelineEvent along with a boolean that
* determines if the current user has participated in that event
*/
data class ThreadTimelineEvent(
val timelineEvent: TimelineEvent,
val isParticipating: Boolean
)

View file

@ -375,7 +375,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::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.HAS_UNREAD_THREAD_MESSAGES, Boolean::class.java)
?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java)
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
}
}

View file

@ -21,9 +21,14 @@ import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
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.query.findIncludingEvent
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
@ -31,7 +36,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
* of threads included. If there is no root thread event no action is done
*/
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(isInitialSync: Boolean = false, currentUserId: String? = null) {
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(roomId: String, realm: Realm, currentUserId: String) {
if (!BuildConfig.THREADING_ENABLED) return
@ -47,13 +52,14 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(isInitialSync:
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
rootThreadEvent?.markEventAsRoot(
isInitialSync = isInitialSync,
currentUserId = currentUserId,
threadsCounted = it.size,
latestMessageTimelineEventEntity = latestMessage
)
}
}
updateNotificationsNew(roomId, realm, currentUserId)
}
/**
@ -71,19 +77,11 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? =
* Mark or update the current event a root thread event
*/
internal fun EventEntity.markEventAsRoot(
isInitialSync: Boolean,
currentUserId: String?,
threadsCounted: Int,
latestMessageTimelineEventEntity: TimelineEventEntity?) {
isRootThread = true
numberOfThreads = threadsCounted
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
}
}
/**
@ -115,7 +113,9 @@ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoo
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
.equalTo(TimelineEventEntityFields.ROOT.HAS_UNREAD_THREAD_MESSAGES, true)
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name)
.or()
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name)
/**
* Returns whether or not the given user is participating in a current thread
@ -131,3 +131,115 @@ internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Re
.findFirst()
?.let { true }
?: false
/**
* Returns whether or not the given user is mentioned in a current thread
* @param roomId the room that the thread exists
* @param rootThreadEventId the thread that the search will be done
* @param userId the user that will try to find if there is a mention
*/
internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, roomId: String, rootThreadEventId: String, userId: String): Boolean =
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.equalTo(TimelineEventEntityFields.ROOT.SENDER, userId)
.findAll()
.firstOrNull { isUserMentioned(userId, it) }
?.let { true }
?: false
/**
* Find the read receipt for the current user
*/
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
.findFirst()
?.eventId
/**
* Returns whether or not the user is mentioned in the event
*/
internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean {
val decryptedContent = timelineEventEntity?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
return decryptedContent.contains(currentUserId.replace("@", "").substringBefore(":"))
}
/**
* Update badge notifications. Count the number of new thread events after the latest
* read receipt and aggregate. This function will find and notify new thread events
* that the user is either mentioned, or the user had participated in.
* Important: If the root thread event is not fetched notification will not work
* Important: It will work only with the latest chunk, while read marker will be changed
* immediately so we should not display wrong notifications
*/
internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
val readReceiptChunk = ChunkEntity
.findIncludingEvent(realm, readReceipt) ?: return
val readReceiptChunkTimelineEvents = readReceiptChunk
.timelineEvents
.where()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll() ?: return
val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
if (readReceiptChunkPosition != -1 && readReceiptChunkPosition != readReceiptChunkTimelineEvents.lastIndex) {
// If the read receipt is found inside the chunk
val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
.slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
.filter { it.root?.isThread() == true }
// In order for the below code to work for old events, we should save the previous read receipt
// and then continue with the chunk search for that read receipt
/*
val newThreadEventsList = arrayListOf<TimelineEventEntity>()
newThreadEventsList.addAll(threadEventsAfterReadReceipt)
// got from latest chunk all new threads, lets move to the others
var nextChunk = ChunkEntity
.find(realm = realm, roomId = roomId, nextToken = readReceiptChunk.nextToken)
.takeIf { readReceiptChunk.nextToken != null }
while (nextChunk != null) {
newThreadEventsList.addAll(nextChunk.timelineEvents
.filter { it.root?.isThread() == true })
nextChunk = ChunkEntity
.find(realm = realm, roomId = roomId, nextToken = nextChunk.nextToken)
.takeIf { readReceiptChunk.nextToken != null }
}*/
// Find if the user is mentioned in those events
val userMentionsList = threadEventsAfterReadReceipt
.filter {
isUserMentioned(currentUserId = currentUserId, it)
}.map {
it.root?.rootThreadEventId
}
// Find the root events in the new thread events
val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
// Update root thread events only if the user have participated in
rootThreads.forEach { eventId ->
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
realm = realm,
roomId = roomId,
rootThreadEventId = eventId,
senderId = currentUserId)
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
if (isUserParticipating) {
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
}
if (userMentionsList.contains(eventId)) {
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
}
}
}
}

View file

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.di.MoshiProvider
@ -55,9 +56,9 @@ internal object EventMapper {
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
eventEntity.decryptionErrorCode = event.mCryptoError?.name
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
eventEntity.hasUnreadThreadMessages = event.threadDetails?.hasUnreadMessage ?: false
eventEntity.rootThreadEventId = event.getRootThreadEventId()
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
eventEntity.threadNotificationState = event.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE
return eventEntity
}
@ -100,7 +101,6 @@ internal object EventMapper {
MXCryptoError.ErrorType.valueOf(errorCode)
}
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
it.threadDetails = ThreadDetails(
isRootThread = eventEntity.isRootThread,
numberOfThreads = eventEntity.numberOfThreads,
@ -112,7 +112,7 @@ internal object EventMapper {
avatarUrl = timelineEventEntity.senderAvatar
)
},
hasUnreadMessage = eventEntity.hasUnreadThreadMessages,
threadNotificationState = eventEntity.threadNotificationState,
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
)
}

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider
@ -46,7 +47,7 @@ internal open class EventEntity(@Index var eventId: String = "",
@Index var isRootThread: Boolean = false,
@Index var rootThreadEventId: String? = null,
var numberOfThreads: Int = 0,
var hasUnreadThreadMessages: Boolean = false,
// var threadNotificationState: Boolean = false,
var threadSummaryLatestMessage: TimelineEventEntity? = null
) : RealmObject() {
@ -61,6 +62,15 @@ internal open class EventEntity(@Index var eventId: String = "",
sendStateStr = value.name
}
private var threadNotificationStateStr: String = ThreadNotificationState.NO_NEW_MESSAGE.name
var threadNotificationState: ThreadNotificationState
get() {
return ThreadNotificationState.valueOf(threadNotificationStateStr)
}
set(value) {
threadNotificationStateStr = value.name
}
companion object
fun setDecryptionResult(result: MXEventDecryptionResult) {

View file

@ -169,7 +169,7 @@ internal class DefaultTimeline(
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
.or()
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, it)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll()

View file

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
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.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
@ -41,6 +42,7 @@ 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.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
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.task.TaskExecutor
@ -48,6 +50,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction
internal class DefaultTimelineService @AssistedInject constructor(
@Assisted private val roomId: String,
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val timelineInput: TimelineInput,
@ -137,13 +140,13 @@ internal class DefaultTimelineService @AssistedInject constructor(
)
}
override fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean {
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use {
TimelineEventEntity.isUserParticipatingInThread(
realm = it,
roomId = roomId,
rootThreadEventId = rootThreadEventId,
senderId = senderId)
senderId = userId)
}
}
@ -151,7 +154,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
monarchy.awaitTransaction {
EventEntity.where(
realm = it,
eventId = rootThreadEventId).findFirst()?.hasUnreadThreadMessages = false
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
}
}
}

View file

@ -18,10 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.kotlin.createObject
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
@ -44,8 +41,8 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRootThreadEventId
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
@ -54,7 +51,9 @@ import javax.inject.Inject
/**
* Insert Chunk in DB, and eventually merge with existing chunk event
*/
internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
internal class TokenChunkEventPersistor @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
@UserId private val userId: String) {
/**
* <pre>
@ -213,7 +212,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
}
val eventIds = ArrayList<String>(eventList.size)
val optimizedThreadSummaryMap = hashMapOf<String,EventEntity>()
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) {
return@forEach
@ -260,16 +259,12 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null ||
(chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
if (shouldUpdateSummary) {
// TODO maybe add support to view latest thread message
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
}
if (currentChunk.isValid) {
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
}
// passing isInitialSync = true because we want to disable local notifications
// they do not work properly without the API
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(true)
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
}
}

View file

@ -425,7 +425,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
}
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(insertType == EventInsertType.INITIAL_SYNC, userId)
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
roomId = roomId,
realm = realm,
currentUserId = userId)
// posting new events to timeline if any is registered
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)

View file

@ -94,6 +94,8 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.flow.flow
@ -291,7 +293,14 @@ class RoomDetailViewModel @AssistedInject constructor(
room.flow()
.liveLocalUnreadThreadList()
.execute {
copy(numberOfLocalUnreadThreads = it.invoke()?.size ?: 0)
val threadList = it.invoke()
val isUserMentioned = threadList?.firstOrNull { timelineEvent ->
timelineEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
}?.let { true } ?: false
val numberOfLocalUnreadThreads = threadList?.size ?: 0
copy(threadNotificationBadgeState = ThreadNotificationBadgeState(
numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
isUserMentioned = isUserMentioned))
}
}
@ -1178,6 +1187,7 @@ class RoomDetailViewModel @AssistedInject constructor(
chatEffectManager.delegate = null
chatEffectManager.dispose()
callManager.removeProtocolsCheckerListener(this)
markThreadTimelineAsReadLocal()
super.onCleared()
}
}

View file

@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
@ -68,7 +69,7 @@ data class RoomDetailViewState(
val hasFailedSending: Boolean = false,
val jitsiState: JitsiState = JitsiState(),
val rootThreadEventId: String? = null,
val numberOfLocalUnreadThreads: Int = 0
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState()
) : MavericksState {
constructor(args: TimelineArgs) : this(

View file

@ -1031,8 +1031,8 @@ class TimelineFragment @Inject constructor(
val badgeFrameLayout = menuThreadList.findViewById<FrameLayout>(R.id.threadNotificationBadgeFrameLayout)
val badgeTextView = menuThreadList.findViewById<TextView>(R.id.threadNotificationBadgeTextView)
val unreadThreadMessages = state.numberOfLocalUnreadThreads
val userIsMentioned = false
val unreadThreadMessages = state.threadNotificationBadgeState.numberOfLocalUnreadThreads
val userIsMentioned = state.threadNotificationBadgeState.isUserMentioned
if (unreadThreadMessages > 0) {
badgeFrameLayout.isVisible = true

View file

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.threads.list.model
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@ -32,6 +33,7 @@ import im.vector.app.core.extensions.setLeftDrawable
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_thread_list)
@ -43,7 +45,7 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
@EpoxyAttribute lateinit var date: String
@EpoxyAttribute lateinit var rootMessage: String
@EpoxyAttribute lateinit var lastMessage: String
@EpoxyAttribute var unreadMessage: Boolean = false
@EpoxyAttribute var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
@EpoxyAttribute lateinit var lastMessageCounter: String
@EpoxyAttribute var rootMessageDeleted: Boolean = false
@EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null
@ -56,11 +58,11 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
holder.avatarImageView.contentDescription = matrixItem.getBestName()
holder.titleTextView.text = title
holder.dateTextView.text = date
if (rootMessageDeleted){
if (rootMessageDeleted) {
holder.rootMessageTextView.text = holder.view.context.getString(R.string.event_redacted)
holder.rootMessageTextView.setLeftDrawable(R.drawable.ic_trash_16, R.attr.colorOnPrimary)
holder.rootMessageTextView.compoundDrawablePadding = DimensionConverter(holder.view.context.resources).dpToPx(10)
}else{
} else {
holder.rootMessageTextView.text = rootMessage
holder.rootMessageTextView.clearDrawables()
}
@ -71,7 +73,24 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName()
holder.lastMessageTextView.text = lastMessage
holder.lastMessageCounterTextView.text = lastMessageCounter
holder.unreadImageView.isVisible = unreadMessage
renderNotificationState(holder)
}
private fun renderNotificationState(holder: Holder) {
when (threadNotificationState) {
ThreadNotificationState.NEW_MESSAGE -> {
holder.unreadImageView.isVisible = true
holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_gray_200));
}
ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE -> {
holder.unreadImageView.isVisible = true
holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_vermilion));
}
else -> {
holder.unreadImageView.isVisible = false
}
}
}
class Holder : VectorEpoxyHolder() {
@ -83,7 +102,6 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
val lastMessageCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
val lastMessageTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
val unreadImageView by bind<ImageView>(R.id.threadSummaryUnreadImageView)
val rootView by bind<ConstraintLayout>(R.id.threadSummaryRootConstraintLayout)
}
}

View file

@ -22,6 +22,7 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.threads.list.model.threadList
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -44,6 +45,15 @@ class ThreadListController @Inject constructor(
val host = this
safeViewState.rootThreadEventList.invoke()
?.filter {
if (safeViewState.shouldFilterThreads) {
it.isParticipating
} else {
true
}
}?.map {
it.timelineEvent
}
?.forEach { timelineEvent ->
val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
threadList {
@ -53,7 +63,7 @@ class ThreadListController @Inject constructor(
title(timelineEvent.senderInfo.displayName)
date(date)
rootMessageDeleted(timelineEvent.root.isRedacted())
unreadMessage(timelineEvent.root.threadDetails?.hasUnreadMessage ?: false)
threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
rootMessage(timelineEvent.root.getDecryptedTextSummary())
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())

View file

@ -29,6 +29,7 @@ import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
import org.matrix.android.sdk.flow.flow
class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState,
@ -52,28 +53,29 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
}
init {
observeThreadsList(initialState.shouldFilterThreads)
observeThreadsList()
}
override fun handle(action: EmptyAction) {}
private fun observeThreadsList(shouldFilterThreads: Boolean) =
room?.flow()
?.liveThreadList()
?.map {
if (!shouldFilterThreads) return@map it
it.filter { timelineEvent ->
room.isUserParticipatingInThread(timelineEvent.eventId, session.myUserId)
}
}
?.flowOn(room.coroutineDispatchers.io)
?.execute { asyncThreads ->
copy(
rootThreadEventList = asyncThreads,
shouldFilterThreads = shouldFilterThreads)
private fun observeThreadsList() {
room?.flow()
?.liveThreadList()
?.map {
it.map { timelineEvent ->
val isParticipating = room.isUserParticipatingInThread(timelineEvent.eventId)
ThreadTimelineEvent(timelineEvent, isParticipating)
}
}
?.flowOn(room.coroutineDispatchers.io)
?.execute { asyncThreads ->
copy(rootThreadEventList = asyncThreads)
}
}
fun applyFiltering(shouldFilterThreads: Boolean) {
observeThreadsList(shouldFilterThreads)
setState {
copy(shouldFilterThreads = shouldFilterThreads)
}
}
}

View file

@ -20,10 +20,10 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
data class ThreadListViewState(
val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized,
val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false,
val roomId: String
) : MavericksState{