From b05c97c265951aa065bd7c202fad5dab6961cea8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Apr 2024 15:43:49 +0200 Subject: [PATCH 1/3] Remove not helping warning. --- .../android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt index 7e7fc51d71a..a64d2733791 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt @@ -48,7 +48,6 @@ class DefaultEncoder @Inject constructor( override fun release() { encoder?.release() - ?: Timber.w("Can't release encoder that is not initialized") encoder = null } } From 8bcaa17630002f2cea7f53780f77dc7c01b0edfc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Jun 2025 12:44:42 +0200 Subject: [PATCH 2/3] Add and improve tests --- .../messages/impl/MessagesPresenterTest.kt | 70 +++++++++---------- .../impl/timeline/TimelinePresenterTest.kt | 33 +++++++-- .../roomlist/impl/RoomListPresenterTest.kt | 21 ++++-- .../matrix/test/room/FakeBaseRoom.kt | 6 +- ...otificationBroadcastReceiverHandlerTest.kt | 10 ++- 5 files changed, 86 insertions(+), 54 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index f45dbe214c1..e42941da0d8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -139,10 +139,10 @@ class MessagesPresenterTest { canRedactOtherResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, canUserPinUnpinResult = { Result.success(true) }, + markAsReadResult = { lambdaError() } ), typingNoticeResult = { Result.success(Unit) }, ) - assertThat(room.baseRoom.markAsReadCalls).isEmpty() val presenter = createMessagesPresenter(joinedRoom = room) presenter.testWithLifecycleOwner { runCurrent() @@ -848,12 +848,12 @@ class MessagesPresenterTest { fun `present - permission to redact other`() = runTest { val joinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canRedactOtherResult = { Result.success(true) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(false) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - ), + canRedactOtherResult = { Result.success(true) }, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(false) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), typingNoticeResult = { Result.success(Unit) }, ) val presenter = createMessagesPresenter(joinedRoom = joinedRoom) @@ -897,12 +897,12 @@ class MessagesPresenterTest { val timeline = FakeTimeline() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - ), + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, ) @@ -937,12 +937,12 @@ class MessagesPresenterTest { val analyticsService = FakeAnalyticsService() val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - ), + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, ) @@ -1096,12 +1096,12 @@ class MessagesPresenterTest { } val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - ), + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + ), liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) }, ) @@ -1134,16 +1134,16 @@ class MessagesPresenterTest { fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - sessionId = A_SESSION_ID, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserJoinCallResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) - ).apply { - givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) - }, + sessionId = A_SESSION_ID, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + canUserJoinCallResult = { Result.success(true) }, + canUserPinUnpinResult = { Result.success(true) }, + initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) + ).apply { + givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) + }, typingNoticeResult = { Result.success(Unit) }, ) val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) }) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 9cbce38709f..1177a72926f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -119,9 +119,26 @@ import kotlin.time.Duration.Companion.seconds } } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) { + fun `present - on scroll finished mark a room as read if the first visible index is 0 - read private`() { + `present - on scroll finished mark a room as read if the first visible index is 0`( + isSendPublicReadReceiptsEnabled = false, + expectedReceiptType = ReceiptType.READ_PRIVATE, + ) + } + + @Test + fun `present - on scroll finished mark a room as read if the first visible index is 0 - read`() { + `present - on scroll finished mark a room as read if the first visible index is 0`( + isSendPublicReadReceiptsEnabled = true, + expectedReceiptType = ReceiptType.READ, + ) + } + + private fun `present - on scroll finished mark a room as read if the first visible index is 0`( + isSendPublicReadReceiptsEnabled: Boolean, + expectedReceiptType: ReceiptType, + ) = runTest(StandardTestDispatcher()) { val timeline = FakeTimeline( timelineItems = flowOf( listOf( @@ -129,11 +146,15 @@ import kotlin.time.Duration.Companion.seconds ) ) ) + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } val room = FakeJoinedRoom( liveTimeline = timeline, - baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) }) + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + markAsReadResult = markAsReadResult, + ) ) - val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) + val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled) val presenter = createTimelinePresenter( timeline = timeline, room = room, @@ -145,7 +166,9 @@ import kotlin.time.Duration.Companion.seconds val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) runCurrent() - assertThat(room.baseRoom.markAsReadCalls).isNotEmpty() + assert(markAsReadResult) + .isCalledOnce() + .with(value(expectedReceiptType)) cancelAndIgnoreRemainingEvents() } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 5726eb1eb0b..6edc2bd5bd8 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -504,9 +504,18 @@ class RoomListPresenterTest { @Test fun `present - check that the room is marked as read with correct RR and as unread`() = runTest { - val room = FakeBaseRoom() - val room2 = FakeBaseRoom(roomId = A_ROOM_ID_2) - val room3 = FakeBaseRoom(roomId = A_ROOM_ID_3) + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val markAsReadResult3 = lambdaRecorder> { Result.success(Unit) } + val room = FakeBaseRoom( + markAsReadResult = markAsReadResult, + ) + val room2 = FakeBaseRoom( + roomId = A_ROOM_ID_2, + ) + val room3 = FakeBaseRoom( + roomId = A_ROOM_ID_3, + markAsReadResult = markAsReadResult3, + ) val allRooms = setOf(room, room2, room3) val sessionPreferencesStore = InMemorySessionPreferencesStore() val matrixClient = FakeMatrixClient().apply { @@ -530,21 +539,19 @@ class RoomListPresenterTest { }.test { val initialState = awaitItem() allRooms.forEach { - assertThat(it.markAsReadCalls).isEmpty() assertThat(it.setUnreadFlagCalls).isEmpty() } initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) - assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) + markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ)) assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false)) clearMessagesForRoomLambda.assertions().isCalledOnce() .with(value(A_SESSION_ID), value(A_ROOM_ID)) initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID_2)) - assertThat(room2.markAsReadCalls).isEmpty() assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true)) // Test again with private read receipts sessionPreferencesStore.setSendPublicReadReceipts(false) initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID_3)) - assertThat(room3.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ_PRIVATE)) + markAsReadResult3.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE)) assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false)) clearMessagesForRoomLambda.assertions().isCalledExactly(2) .withSequence( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index d755722bd39..ae9aee87361 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -54,6 +54,7 @@ class FakeBaseRoom( private val canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, private val canUserPinUnpinResult: (UserId) -> Result = { lambdaError() }, private val setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, + private val markAsReadResult: (ReceiptType) -> Result = { Result.success(Unit) }, private val powerLevelsResult: () -> Result = { lambdaError() }, private val leaveRoomLambda: () -> Result = { lambdaError() }, private val updateMembersResult: () -> Unit = { lambdaError() }, @@ -183,11 +184,8 @@ class FakeBaseRoom( return setIsFavoriteResult(isFavorite) } - val markAsReadCalls = mutableListOf() - override suspend fun markAsRead(receiptType: ReceiptType): Result { - markAsReadCalls.add(receiptType) - return Result.success(Unit) + return markAsReadResult(receiptType) } var setUnreadFlagCalls = mutableListOf() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt index ac53bace79d..c51e7ad7a85 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent -import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -224,7 +223,12 @@ class NotificationBroadcastReceiverHandlerTest { getLambda = getLambda ) val clearMessagesForRoomLambda = lambdaRecorder { _, _ -> } - val joinedRoom = FakeJoinedRoom() + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + markAsReadResult = markAsReadResult, + ), + ) val fakeNotificationCleaner = FakeNotificationCleaner( clearMessagesForRoomLambda = clearMessagesForRoomLambda, ) @@ -243,7 +247,7 @@ class NotificationBroadcastReceiverHandlerTest { clearMessagesForRoomLambda.assertions() .isCalledOnce() .with(value(A_SESSION_ID), value(A_ROOM_ID)) - assertThat(joinedRoom.baseRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType)) + markAsReadResult.assertions().isCalledOnce().with(value(expectedReceiptType)) } @Test From ba1776c9367fc8b2bc18b0bcef121805b8446233 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Apr 2024 15:40:11 +0200 Subject: [PATCH 3/3] Send the `m.fully_read` read marker when the user navigates back to the room list, to mark the room as read. --- .../messages/impl/timeline/MarkAsFullyRead.kt | 37 +++++++++++++ .../impl/timeline/TimelinePresenter.kt | 8 +++ .../timeline/DefaultMarkAsFullyReadTest.kt | 55 +++++++++++++++++++ .../impl/timeline/FakeMarkAsFullyRead.kt | 19 +++++++ .../impl/timeline/TimelinePresenterTest.kt | 28 +++++++++- 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt new file mode 100644 index 00000000000..b2962e2f5ad --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +interface MarkAsFullyRead { + operator fun invoke(roomId: RoomId) +} + +@ContributesBinding(SessionScope::class) +class DefaultMarkAsFullyRead @Inject constructor( + private val matrixClient: MatrixClient, +) : MarkAsFullyRead { + override fun invoke(roomId: RoomId) { + matrixClient.sessionCoroutineScope.launch { + matrixClient.getRoom(roomId)?.use { room -> + room.markAsRead(receiptType = ReceiptType.FULLY_READ) + .onFailure { + Timber.e("Failed to mark room $roomId as fully read", it) + } + } + } + } +} 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 3be27fe7d0a..2ca61fc8fee 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 @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState @@ -76,6 +77,7 @@ class TimelinePresenter @AssistedInject constructor( private val resolveVerifiedUserSendFailurePresenter: Presenter, private val typingNotificationPresenter: Presenter, private val roomCallStatePresenter: Presenter, + private val markAsFullyRead: MarkAsFullyRead, ) : Presenter { @AssistedFactory interface Factory { @@ -177,6 +179,12 @@ class TimelinePresenter @AssistedInject constructor( } } + DisposableEffect(Unit) { + onDispose { + markAsFullyRead(room.roomId) + } + } + LaunchedEffect(Unit) { timelineItemsFactory.timelineItems .onEach { newTimelineItems -> diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt new file mode 100644 index 00000000000..6340ce56a5c --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.impl.timeline + +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultMarkAsFullyReadTest { + @Test + fun `When room is not found, then no exception is thrown`() = runTest { + val markAsFullyRead = DefaultMarkAsFullyRead( + FakeMatrixClient( + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult(A_ROOM_ID, null) + } + ) + markAsFullyRead.invoke(A_ROOM_ID) + runCurrent() + } + + @Test + fun `When room is found, the expected method is invoked`() = runTest { + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val baseRoom = FakeBaseRoom( + markAsReadResult = markAsReadResult + ) + val markAsFullyRead = DefaultMarkAsFullyRead( + FakeMatrixClient( + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult(A_ROOM_ID, baseRoom) + } + ) + markAsFullyRead.invoke(A_ROOM_ID) + runCurrent() + markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.FULLY_READ)) + baseRoom.assertDestroyed() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt new file mode 100644 index 00000000000..895676a1260 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeMarkAsFullyRead( + private val invokeResult: (RoomId) -> Unit = { lambdaError() } +) : MarkAsFullyRead { + override fun invoke(roomId: RoomId) { + invokeResult(roomId) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 1177a72926f..32b48f8a82b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem @@ -40,6 +41,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID @@ -58,6 +60,7 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,7 +79,9 @@ import java.util.Date import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest { +@Suppress("LargeClass") +@OptIn(ExperimentalCoroutinesApi::class) +class TimelinePresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -173,6 +178,25 @@ import kotlin.time.Duration.Companion.seconds } } + @Test + fun `present - once presenter is disposed, room is marked as fully read`() = runTest { + val invokeResult = lambdaRecorder { } + val presenter = createTimelinePresenter( + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + canUserSendMessageResult = { _, _ -> Result.success(true) }, + ) + ), + markAsFullyRead = FakeMarkAsFullyRead( + invokeResult = invokeResult, + ) + ) + presenter.test { + awaitFirstItem() + } + invokeResult.assertions().isCalledOnce().with(value(A_ROOM_ID)) + } + @Test fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType -> @@ -697,6 +721,7 @@ import kotlin.time.Duration.Companion.seconds sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), + markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -713,6 +738,7 @@ import kotlin.time.Duration.Companion.seconds resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, typingNotificationPresenter = { aTypingNotificationState() }, roomCallStatePresenter = { aStandByCallState() }, + markAsFullyRead = markAsFullyRead, ) } }