diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 986dba4709..a37f6419a4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -159,10 +159,10 @@ class MessagesPresenter @AssistedInject constructor( } LaunchedEffect(Unit) { - // Mark the room as read on entering but don't send read receipts + // Remove the unread flag on entering but don't send read receipts // as those will be handled by the timeline. withContext(dispatchers.io) { - room.markAsRead(null) + room.setUnreadFlag(isUnread = false) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index c044d96f5c..89156f80e7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -232,12 +232,17 @@ class TimelinePresenter @AssistedInject constructor( lastReadReceiptId: MutableState, readReceiptType: ReceiptType, ) = launch(dispatchers.computation) { - // Get last valid EventId seen by the user, as the first index might refer to a Virtual item - val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) - if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { - lastReadReceiptIndex.value = firstVisibleIndex - lastReadReceiptId.value = eventId - timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) + // If we are at the bottom of timeline, we mark the room as read. + if (firstVisibleIndex == 0) { + room.markAsRead(receiptType = readReceiptType) + } else { + // Get last valid EventId seen by the user, as the first index might refer to a Virtual item + val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) + if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { + lastReadReceiptIndex.value = firstVisibleIndex + lastReadReceiptId.value = eventId + timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) + } } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index fa841ff88f..e14a2b9aaa 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -145,15 +145,20 @@ class RoomListPresenter @Inject constructor( is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) is RoomListEvents.MarkAsRead -> coroutineScope.launch { - val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) { - ReceiptType.READ - } else { - ReceiptType.READ_PRIVATE + client.getRoom(event.roomId)?.use { room -> + room.setUnreadFlag(isUnread = false) + val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) { + ReceiptType.READ + } else { + ReceiptType.READ_PRIVATE + } + room.markAsRead(receiptType) } - client.getRoom(event.roomId)?.markAsRead(receiptType) } is RoomListEvents.MarkAsUnread -> coroutineScope.launch { - client.getRoom(event.roomId)?.markAsUnread() + client.getRoom(event.roomId)?.use { room -> + room.setUnreadFlag(isUnread = true) + } } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 49d7eb2974..6ff8df00f0 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -487,18 +487,18 @@ class RoomListPresenterTests { }.test { val initialState = awaitItem() assertThat(room.markAsReadCalls).isEmpty() - assertThat(room.markAsUnreadReadCallCount).isEqualTo(0) + assertThat(room.setUnreadFlagCalls).isEmpty() initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) - assertThat(room.markAsUnreadReadCallCount).isEqualTo(0) + assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false)) initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) - assertThat(room.markAsUnreadReadCallCount).isEqualTo(1) + assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true)) // Test again with private read receipts sessionPreferencesStore.setSendPublicReadReceipts(false) initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE)) - assertThat(room.markAsUnreadReadCallCount).isEqualTo(1) + assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true, false)) cancelAndIgnoreRemainingEvents() scope.cancel() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index db5cd6c260..22d8f56415 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -153,15 +153,17 @@ interface MatrixRoom : Closeable { suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result /** - * Reverts a previously set unread flag, and eventually send a Read Receipt. - * @param receiptType The type of receipt to send. If null, no Read Receipt will be sent. + * Mark the room as read by trying to attach an unthreaded read receipt to the latest room event. + * @param receiptType The type of receipt to send. */ - suspend fun markAsRead(receiptType: ReceiptType?): Result + suspend fun markAsRead(receiptType: ReceiptType): Result /** - * Sets a flag on the room to indicate that the user has explicitly marked it as unread. + * Sets a flag on the room to indicate that the user has explicitly marked it as unread, or reverts the flag. + * @param isUnread true to mark the room as unread, false to remove the flag. + * */ - suspend fun markAsUnread(): Result + suspend fun setUnreadFlag(isUnread: Boolean): Result /** * Share a location message in the room. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 6ab29c1840..3ce35cfaa6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -442,19 +442,15 @@ class RustMatrixRoom( } } - override suspend fun markAsRead(receiptType: ReceiptType?): Result = withContext(roomDispatcher) { + override suspend fun markAsRead(receiptType: ReceiptType): Result = withContext(roomDispatcher) { runCatching { - if (receiptType != null) { - innerRoom.markAsReadAndSendReadReceipt(receiptType.toRustReceiptType()) - } else { - innerRoom.markAsRead() - } + innerRoom.markAsRead(receiptType.toRustReceiptType()) } } - override suspend fun markAsUnread(): Result = withContext(roomDispatcher) { + override suspend fun setUnreadFlag(isUnread: Boolean): Result = withContext(roomDispatcher) { runCatching { - innerRoom.markAsUnread() + innerRoom.setUnreadFlag(isUnread) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 60969cfd0b..b3bfa66328 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -378,17 +378,18 @@ class FakeMatrixRoom( return reportContentResult } - val markAsReadCalls = mutableListOf() - override suspend fun markAsRead(receiptType: ReceiptType?): Result { + val markAsReadCalls = mutableListOf() + + override suspend fun markAsRead(receiptType: ReceiptType): Result { markAsReadCalls.add(receiptType) return Result.success(Unit) } - var markAsUnreadReadCallCount = 0 + var setUnreadFlagCalls = mutableListOf() private set - override suspend fun markAsUnread(): Result { - markAsUnreadReadCallCount++ + override suspend fun setUnreadFlag(isUnread: Boolean): Result { + setUnreadFlagCalls.add(isUnread) return Result.success(Unit) }