From c40a686cff5e01489b23f9b59fc32ddfe5bfb2f5 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Fri, 3 Dec 2021 18:15:25 +0000 Subject: [PATCH] Implement LOCAL thread notifications that work only on real time. --- .../org/matrix/android/sdk/flow/FlowRoom.kt | 7 +++++ .../session/room/timeline/TimelineService.kt | 17 +++++++++++ .../sdk/api/session/threads/ThreadDetails.kt | 3 +- .../database/RealmSessionStoreMigration.kt | 1 + .../database/helper/ThreadEventsHelper.kt | 27 +++++++++++++++-- .../internal/database/mapper/EventMapper.kt | 2 ++ .../internal/database/model/EventEntity.kt | 1 + .../room/timeline/DefaultTimelineService.kt | 25 ++++++++++++++++ .../room/timeline/TokenChunkEventPersistor.kt | 4 ++- .../sync/handler/room/RoomSyncHandler.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 26 +++++++++++++++++ .../home/room/detail/RoomDetailViewState.kt | 5 ++-- .../home/room/detail/TimelineFragment.kt | 4 +-- .../threads/list/model/ThreadListModel.kt | 5 ++++ .../list/viewmodel/ThreadListController.kt | 1 + .../src/main/res/layout/item_thread_list.xml | 29 ++++++++++++++----- 16 files changed, 141 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7091905991..cdb3bdf9c2 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -106,6 +106,13 @@ class FlowRoom(private val room: Room) { room.getAllThreads() } } + + fun liveLocalUnreadThreadList(): Flow> { + return room.getNumberOfLocalThreadNotificationsLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getNumberOfLocalThreadNotifications() + } + } } fun Room.flow(): FlowRoom { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 6b1ad5554b..068fa87a66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -68,11 +68,28 @@ interface TimelineService { */ fun getAllThreads(): List + /** + * Get a live list of all the local unread threads for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getNumberOfLocalThreadNotificationsLive(): LiveData> + + /** + * Get a list of all the local unread threads for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getNumberOfLocalThreadNotifications(): List + /** * 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 + /** + * Marks the current thread as read. This is a local implementation + * @param rootThreadEventId the eventId of the current thread + */ + suspend fun markThreadAsRead(rootThreadEventId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index 04dbb18797..62568cdce1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -22,5 +22,6 @@ data class ThreadDetails( val isRootThread: Boolean = false, val numberOfThreads: Int = 0, val threadSummarySenderInfo: SenderInfo? = null, - val threadSummaryLatestTextMessage: String? = null + val threadSummaryLatestTextMessage: String? = null, + val hasUnreadMessage: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 111fc50e56..301a479d01 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -375,6 +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) ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index aa3ba0fc25..34bc117ddf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -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 * of threads included. If there is no root thread event no action is done */ -internal fun Map.updateThreadSummaryIfNeeded() { +internal fun Map.updateThreadSummaryIfNeeded(isInitialSync: Boolean = false, currentUserId: String? = null) { if (!BuildConfig.THREADING_ENABLED) return @@ -47,6 +47,8 @@ internal fun Map.updateThreadSummaryIfNeeded() { val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity rootThreadEvent?.markEventAsRoot( + isInitialSync = isInitialSync, + currentUserId = currentUserId, threadsCounted = it.size, latestMessageTimelineEventEntity = latestMessage ) @@ -68,11 +70,20 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? = /** * Mark or update the current event a root thread event */ -internal fun EventEntity.markEventAsRoot(threadsCounted: Int, - latestMessageTimelineEventEntity: TimelineEventEntity?) { +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 + } } /** @@ -96,6 +107,16 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) .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 + .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 * @param roomId the room that the thread exists diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index cf16138196..319d91b12a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -55,6 +55,7 @@ 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 return eventEntity @@ -111,6 +112,7 @@ internal object EventMapper { avatarUrl = timelineEventEntity.senderAvatar ) }, + hasUnreadMessage = eventEntity.hasUnreadThreadMessages, threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 1898d63af8..1ba4d564bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -46,6 +46,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 threadSummaryLatestMessage: TimelineEventEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 2335f7bcd2..3f702abde8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -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.util.Optional 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.isUserParticipatingInThread 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.TimelineEventEntityFields 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.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultTimelineService @AssistedInject constructor( @Assisted private val roomId: String, @@ -106,6 +109,20 @@ internal class DefaultTimelineService @AssistedInject constructor( } } + override fun getNumberOfLocalThreadNotificationsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getNumberOfLocalThreadNotifications(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + override fun getAllThreadsLive(): LiveData> { return monarchy.findAllMappedWithChanges( { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, @@ -129,4 +146,12 @@ internal class DefaultTimelineService @AssistedInject constructor( senderId = senderId) } } + + override suspend fun markThreadAsRead(rootThreadEventId: String) { + monarchy.awaitTransaction { + EventEntity.where( + realm = it, + eventId = rootThreadEventId).findFirst()?.hasUnreadThreadMessages = false + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index f6441c9d60..2fa298a171 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -267,7 +267,9 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri 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) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 8d64c7fc96..8c258e7d91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -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 timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 907ca360bb..cbe5e542fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -189,8 +189,12 @@ class RoomDetailViewModel @AssistedInject constructor( if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { prepareForEncryption() } + markThreadTimelineAsReadLocal() + observeLocalThreadNotifications() } + + private fun observeDataStore() { viewModelScope.launch { 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 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) { timelineEvents.tryEmit(snapshot) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index fa772ca073..df6c75d30c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -67,8 +67,9 @@ data class RoomDetailViewState( val isAllowedToStartWebRTCCall: Boolean = true, val hasFailedSending: Boolean = false, val jitsiState: JitsiState = JitsiState(), - val rootThreadEventId: String? = null - ) : MavericksState { + val rootThreadEventId: String? = null, + val numberOfLocalUnreadThreads: Int = 0 +) : MavericksState { constructor(args: TimelineArgs) : this( roomId = args.roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index b37ba12f37..f12ca9e84c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1031,9 +1031,9 @@ class TimelineFragment @Inject constructor( val badgeFrameLayout = menuThreadList.findViewById(R.id.threadNotificationBadgeFrameLayout) val badgeTextView = menuThreadList.findViewById(R.id.threadNotificationBadgeTextView) - val unreadThreadMessages = 18 + state.pushCounter + val unreadThreadMessages = state.numberOfLocalUnreadThreads + val userIsMentioned = false - val userIsMentioned = true if (unreadThreadMessages > 0) { badgeFrameLayout.isVisible = true badgeTextView.text = unreadThreadMessages.toString() diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt index f47f6f46cc..f3aac46ed3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt @@ -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.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -42,6 +43,7 @@ abstract class ThreadListModel : VectorEpoxyModel() { @EpoxyAttribute lateinit var date: String @EpoxyAttribute lateinit var rootMessage: String @EpoxyAttribute lateinit var lastMessage: String + @EpoxyAttribute var unreadMessage: Boolean = false @EpoxyAttribute lateinit var lastMessageCounter: String @EpoxyAttribute var rootMessageDeleted: Boolean = false @EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null @@ -69,6 +71,7 @@ abstract class ThreadListModel : VectorEpoxyModel() { holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName() holder.lastMessageTextView.text = lastMessage holder.lastMessageCounterTextView.text = lastMessageCounter + holder.unreadImageView.isVisible = unreadMessage } class Holder : VectorEpoxyHolder() { @@ -79,6 +82,8 @@ abstract class ThreadListModel : VectorEpoxyModel() { val lastMessageAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView) val lastMessageCounterTextView by bind(R.id.messageThreadSummaryCounterTextView) val lastMessageTextView by bind(R.id.messageThreadSummaryInfoTextView) + val unreadImageView by bind(R.id.threadSummaryUnreadImageView) + val rootView by bind(R.id.threadSummaryRootConstraintLayout) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index d17dee6e51..6e07f0a95f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -53,6 +53,7 @@ class ThreadListController @Inject constructor( title(timelineEvent.senderInfo.displayName) date(date) rootMessageDeleted(timelineEvent.root.isRedacted()) + unreadMessage(timelineEvent.root.threadDetails?.hasUnreadMessage ?: false) rootMessage(timelineEvent.root.getDecryptedTextSummary()) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) diff --git a/vector/src/main/res/layout/item_thread_list.xml b/vector/src/main/res/layout/item_thread_list.xml index 8cf93c5404..6a1d075b7c 100644 --- a/vector/src/main/res/layout/item_thread_list.xml +++ b/vector/src/main/res/layout/item_thread_list.xml @@ -1,18 +1,17 @@ - + android:foreground="?attr/selectableItemBackground" + android:paddingStart="12dp" + android:paddingTop="12dp" + android:paddingEnd="0dp"> + +