diff --git a/changelog.d/2261.feature b/changelog.d/2261.feature new file mode 100644 index 0000000000..c643cdf43a --- /dev/null +++ b/changelog.d/2261.feature @@ -0,0 +1 @@ +Manually mark a room as unread 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 5f21e5b348..1b264ac1f7 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 @@ -155,6 +155,14 @@ class MessagesPresenter @AssistedInject constructor( mutableStateOf(false) } + LaunchedEffect(Unit) { + // Mark the room as read on entering but don't send read receipts + // as those will be handled by the timeline. + withContext(dispatchers.io) { + room.markAsRead(null) + } + } + LaunchedEffect(syncUpdateFlow.value) { withContext(dispatchers.io) { canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false) 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 9dd9ff5d88..fe7e07e6f9 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 @@ -96,7 +96,9 @@ import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -129,6 +131,21 @@ class MessagesPresenterTest { } } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - check that the room is marked as read`() = runTest { + val room = FakeMatrixRoom() + assertThat(room.markAsReadCalls).isEmpty() + val presenter = createMessagesPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + runCurrent() + assertThat(room.markAsReadCalls).isEqualTo(listOf(null)) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { val room = FakeMatrixRoom().apply { diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 4e415fd2a2..61f40b53ec 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(projects.libraries.eventformatter.api) implementation(projects.libraries.indicator.api) implementation(projects.libraries.deeplink) + implementation(projects.libraries.preferences.api) implementation(projects.features.invitelist.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.leaveroom.api) @@ -59,6 +60,8 @@ dependencies { api(projects.features.roomlist.api) ksp(libs.showkase.processor) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) @@ -71,6 +74,7 @@ dependencies { testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.permissions.noop) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.features.invitelist.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt index 1d2cf2c910..cdd8c4527d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt @@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun RoomListContextMenu( contextMenu: RoomListState.ContextMenu.Shown, - eventSink: (RoomListEvents) -> Unit, + eventSink: (RoomListEvents.RoomListBottomSheetEvents) -> Unit, onRoomSettingsClicked: (roomId: RoomId) -> Unit, ) { ModalBottomSheet( @@ -49,9 +49,17 @@ fun RoomListContextMenu( ) { RoomListModalBottomSheetContent( contextMenu = contextMenu, + onRoomMarkReadClicked = { + eventSink(RoomListEvents.HideContextMenu) + eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId)) + }, + onRoomMarkUnreadClicked = { + eventSink(RoomListEvents.HideContextMenu) + eventSink(RoomListEvents.MarkAsUnread(contextMenu.roomId)) + }, onRoomSettingsClicked = { eventSink(RoomListEvents.HideContextMenu) - onRoomSettingsClicked(it) + onRoomSettingsClicked(contextMenu.roomId) }, onLeaveRoomClicked = { eventSink(RoomListEvents.HideContextMenu) @@ -64,8 +72,10 @@ fun RoomListContextMenu( @Composable private fun RoomListModalBottomSheetContent( contextMenu: RoomListState.ContextMenu.Shown, - onRoomSettingsClicked: (roomId: RoomId) -> Unit, - onLeaveRoomClicked: (roomId: RoomId) -> Unit, + onRoomMarkReadClicked: () -> Unit, + onRoomMarkUnreadClicked: () -> Unit, + onRoomSettingsClicked: () -> Unit, + onLeaveRoomClicked: () -> Unit, ) { Column( modifier = Modifier.fillMaxWidth() @@ -78,6 +88,38 @@ private fun RoomListModalBottomSheetContent( ) } ) + if (contextMenu.markAsUnreadFeatureFlagEnabled) { + ListItem( + headlineContent = { + Text( + text = stringResource( + id = if (contextMenu.hasNewContent) { + R.string.screen_roomlist_mark_as_read + } else { + R.string.screen_roomlist_mark_as_unread + } + ), + style = MaterialTheme.typography.bodyLarge, + ) + }, + modifier = Modifier.clickable { + if (contextMenu.hasNewContent) { + onRoomMarkReadClicked() + } else { + onRoomMarkUnreadClicked() + } + }, + /* TODO Design + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector( + CompoundIcons.Settings, + contentDescription = stringResource(id = CommonStrings.common_settings) + ) + ), + */ + style = ListItemStyle.Primary, + ) + } ListItem( headlineContent = { Text( @@ -85,7 +127,7 @@ private fun RoomListModalBottomSheetContent( style = MaterialTheme.typography.bodyLarge, ) }, - modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) }, + modifier = Modifier.clickable { onRoomSettingsClicked() }, leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector( CompoundIcons.Settings, @@ -96,14 +138,16 @@ private fun RoomListModalBottomSheetContent( ) ListItem( headlineContent = { - val leaveText = stringResource(id = if (contextMenu.isDm) { - CommonStrings.action_leave_conversation - } else { - CommonStrings.action_leave_room - }) + val leaveText = stringResource( + id = if (contextMenu.isDm) { + CommonStrings.action_leave_conversation + } else { + CommonStrings.action_leave_room + } + ) Text(text = leaveText) }, - modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) }, + modifier = Modifier.clickable { onLeaveRoomClicked() }, leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector( CompoundIcons.Leave, @@ -122,11 +166,9 @@ private fun RoomListModalBottomSheetContent( @Composable internal fun RoomListModalBottomSheetContentPreview() = ElementPreview { RoomListModalBottomSheetContent( - contextMenu = RoomListState.ContextMenu.Shown( - roomId = RoomId(value = "!aRoom:aDomain"), - roomName = "aRoom", - isDm = false, - ), + contextMenu = aContextMenuShown(hasNewContent = true), + onRoomMarkReadClicked = {}, + onRoomMarkUnreadClicked = {}, onRoomSettingsClicked = {}, onLeaveRoomClicked = {} ) @@ -136,11 +178,9 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview { @Composable internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview { RoomListModalBottomSheetContent( - contextMenu = RoomListState.ContextMenu.Shown( - roomId = RoomId(value = "!aRoom:aDomain"), - roomName = "aRoom", - isDm = true, - ), + contextMenu = aContextMenuShown(isDm = true), + onRoomMarkReadClicked = {}, + onRoomMarkUnreadClicked = {}, onRoomSettingsClicked = {}, onLeaveRoomClicked = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index d54bc604cf..affd3946f2 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -26,6 +26,10 @@ sealed interface RoomListEvents { data object DismissRecoveryKeyPrompt : RoomListEvents data object ToggleSearchResults : RoomListEvents data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - data object HideContextMenu : RoomListEvents - data class LeaveRoom(val roomId: RoomId) : RoomListEvents + + sealed interface RoomListBottomSheetEvents : RoomListEvents + data object HideContextMenu : RoomListBottomSheetEvents + data class LeaveRoom(val roomId: RoomId) : RoomListBottomSheetEvents + data class MarkAsRead(val roomId: RoomId) : RoomListBottomSheetEvents + data class MarkAsUnread(val roomId: RoomId) : RoomListBottomSheetEvents } 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 f25ebcec20..fa841ff88f 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 @@ -25,12 +25,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter @@ -44,10 +46,12 @@ import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -65,9 +69,11 @@ class RoomListPresenter @Inject constructor( private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, private val migrationScreenPresenter: MigrationScreenPresenter, + private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { @Composable override fun present(): RoomListState { + val coroutineScope = rememberCoroutineScope() val leaveRoomState = leaveRoomPresenter.present() val matrixUser: MutableState = rememberSaveable { mutableStateOf(null) @@ -105,6 +111,9 @@ class RoomListPresenter @Inject constructor( } } + val markAsUnreadFeatureFlagEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MarkAsUnread) + .collectAsState(initial = null) + // Avatar indicator val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() @@ -129,10 +138,23 @@ class RoomListPresenter @Inject constructor( roomId = event.roomListRoomSummary.roomId, roomName = event.roomListRoomSummary.name, isDm = event.roomListRoomSummary.isDm, + markAsUnreadFeatureFlagEnabled = markAsUnreadFeatureFlagEnabled == true, + hasNewContent = event.roomListRoomSummary.hasNewContent ) } 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)?.markAsRead(receiptType) + } + is RoomListEvents.MarkAsUnread -> coroutineScope.launch { + client.getRoom(event.roomId)?.markAsUnread() + } } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 13ba030720..d2f1a55671 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -49,6 +49,8 @@ data class RoomListState( val roomId: RoomId, val roomName: String, val isDm: Boolean, + val markAsUnreadFeatureFlagEnabled: Boolean, + val hasNewContent: Boolean, ) : ContextMenu } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 7fda3a180a..c975ee069a 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -43,13 +43,7 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(invitesState = InvitesState.NewInvites), aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), aRoomListState().copy(displaySearchResults = true), - aRoomListState().copy( - contextMenu = RoomListState.ContextMenu.Shown( - roomId = RoomId("!aRoom:aDomain"), - roomName = "A nice room name", - isDm = false, - ) - ), + aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState().copy(displayRecoveryKeyPrompt = true), aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), @@ -103,3 +97,15 @@ internal fun aRoomListRoomSummaryList(): ImmutableList { ), ) } + +internal fun aContextMenuShown( + roomName: String = "aRoom", + isDm: Boolean = false, + hasNewContent: Boolean = false, +) = RoomListState.ContextMenu.Shown( + roomId = RoomId("!aRoom:aDomain"), + roomName = roomName, + isDm = isDm, + markAsUnreadFeatureFlagEnabled = true, + hasNewContent = hasNewContent, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt index 2d47be35a3..d3c7c43148 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt @@ -45,6 +45,7 @@ class RoomListRoomSummaryFactory @Inject constructor( numberOfUnreadMessages = 0, numberOfUnreadMentions = 0, numberOfUnreadNotifications = 0, + isMarkedUnread = false, userDefinedNotificationMode = null, hasRoomCall = false, isDm = false, @@ -73,6 +74,7 @@ class RoomListRoomSummaryFactory @Inject constructor( numberOfUnreadMessages = roomSummary.details.numUnreadMessages, numberOfUnreadMentions = roomSummary.details.numUnreadMentions, numberOfUnreadNotifications = roomSummary.details.numUnreadNotifications, + isMarkedUnread = roomSummary.details.isMarkedUnread, timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp), lastMessage = roomSummary.details.lastMessage?.let { message -> roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index 26cd9e2d32..2a5289c9db 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -29,6 +29,7 @@ data class RoomListRoomSummary( val numberOfUnreadMessages: Int, val numberOfUnreadMentions: Int, val numberOfUnreadNotifications: Int, + val isMarkedUnread: Boolean, val timestamp: String?, val lastMessage: CharSequence?, val avatarData: AvatarData, @@ -38,9 +39,11 @@ data class RoomListRoomSummary( val isDm: Boolean, ) { val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && - (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) + (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || + isMarkedUnread val hasNewContent = numberOfUnreadMessages > 0 || numberOfUnreadMentions > 0 || - numberOfUnreadNotifications > 0 + numberOfUnreadNotifications > 0 || + isMarkedUnread } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt index e53e1f9072..9b80edecb0 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt @@ -89,6 +89,7 @@ internal fun aRoomListRoomSummary( numberOfUnreadMessages: Int = 0, numberOfUnreadMentions: Int = 0, numberOfUnreadNotifications: Int = 0, + isMarkedUnread: Boolean = false, lastMessage: String? = "Last message", timestamp: String? = lastMessage?.let { "88:88" }, isPlaceholder: Boolean = false, @@ -103,6 +104,7 @@ internal fun aRoomListRoomSummary( numberOfUnreadMessages = numberOfUnreadMessages, numberOfUnreadMentions = numberOfUnreadMentions, numberOfUnreadNotifications = numberOfUnreadNotifications, + isMarkedUnread = isMarkedUnread, timestamp = timestamp, lastMessage = lastMessage, avatarData = avatarData, diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt new file mode 100644 index 0000000000..6907fac598 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenuTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * 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 io.element.android.features.roomlist.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureCalledOnceWithParam +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomListContextMenuTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on Mark as read generates expected Events`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(hasNewContent = true) + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClicked = EnsureNeverCalledWithParam(), + ) + } + rule.clickOn(R.string.screen_roomlist_mark_as_read) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.MarkAsRead(contextMenu.roomId), + ) + ) + } + + @Test + fun `clicking on Mark as unread generates expected Events`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(hasNewContent = false) + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClicked = EnsureNeverCalledWithParam(), + ) + } + rule.clickOn(R.string.screen_roomlist_mark_as_unread) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.MarkAsUnread(contextMenu.roomId), + ) + ) + } + + @Test + fun `clicking on Leave dm generates expected Events`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(isDm = true) + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClicked = EnsureNeverCalledWithParam(), + ) + } + rule.clickOn(CommonStrings.action_leave_conversation) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.LeaveRoom(contextMenu.roomId), + ) + ) + } + + @Test + fun `clicking on Leave room generates expected Events`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown(isDm = false) + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClicked = EnsureNeverCalledWithParam(), + ) + } + rule.clickOn(CommonStrings.action_leave_room) + eventsRecorder.assertList( + listOf( + RoomListEvents.HideContextMenu, + RoomListEvents.LeaveRoom(contextMenu.roomId), + ) + ) + } + + @Test + fun `clicking on Settings invokes the expected callback and generates expected Event`() { + val eventsRecorder = EventsRecorder() + val contextMenu = aContextMenuShown() + val callback = EnsureCalledOnceWithParam(contextMenu.roomId) + rule.setContent { + RoomListContextMenu( + contextMenu = contextMenu, + eventSink = eventsRecorder, + onRoomSettingsClicked = callback, + ) + } + rule.clickOn(CommonStrings.common_settings) + eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) + callback.assertSuccess() + } +} 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 10730f0c90..49d7eb2974 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 @@ -25,28 +25,30 @@ import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter -import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter +import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_AVATAR_URL @@ -59,6 +61,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService @@ -175,12 +178,23 @@ class RoomListPresenterTests { val initialItems = initialState.roomList.dataOrNull().orEmpty() assertThat(initialItems.size).isEqualTo(16) assertThat(initialItems.all { it.isPlaceholder }).isTrue() - roomListService.postAllRooms(listOf(aRoomSummaryFilled())) + roomListService.postAllRooms( + listOf( + aRoomSummaryFilled( + numUnreadMentions = 1, + numUnreadMessages = 2, + ) + ) + ) val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 1 }.last() val withRoomStateItems = withRoomState.roomList.dataOrNull().orEmpty() assertThat(withRoomStateItems.size).isEqualTo(1) - assertThat(withRoomStateItems.first()) - .isEqualTo(aRoomListRoomSummary) + assertThat(withRoomStateItems.first()).isEqualTo( + createRoomListRoomSummary( + numberOfUnreadMentions = 1, + numberOfUnreadMessages = 2, + ) + ) scope.cancel() } } @@ -196,7 +210,14 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - roomListService.postAllRooms(listOf(aRoomSummaryFilled())) + roomListService.postAllRooms( + listOf( + aRoomSummaryFilled( + numUnreadMentions = 1, + numUnreadMessages = 2, + ) + ) + ) skipItems(3) val loadedState = awaitItem() // Test filtering with result @@ -207,8 +228,12 @@ class RoomListPresenterTests { assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1) assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3)) assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1) - assertThat(withFilteredRoomState.filteredRoomList.first()) - .isEqualTo(aRoomListRoomSummary) + assertThat(withFilteredRoomState.filteredRoomList.first()).isEqualTo( + createRoomListRoomSummary( + numberOfUnreadMentions = 1, + numberOfUnreadMessages = 2, + ) + ) // Test filtering without result withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada")) skipItems(1) @@ -322,12 +347,19 @@ class RoomListPresenterTests { skipItems(1) val initialState = awaitItem() - val summary = aRoomListRoomSummary + val summary = createRoomListRoomSummary() initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) val shownState = awaitItem() - assertThat(shownState.contextMenu) - .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false)) + assertThat(shownState.contextMenu).isEqualTo( + RoomListState.ContextMenu.Shown( + roomId = summary.roomId, + roomName = summary.name, + isDm = false, + markAsUnreadFeatureFlagEnabled = true, + hasNewContent = false, + ) + ) scope.cancel() } } @@ -342,12 +374,19 @@ class RoomListPresenterTests { skipItems(1) val initialState = awaitItem() - val summary = aRoomListRoomSummary + val summary = createRoomListRoomSummary() initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) val shownState = awaitItem() - assertThat(shownState.contextMenu) - .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name, false)) + assertThat(shownState.contextMenu).isEqualTo( + RoomListState.ContextMenu.Shown( + roomId = summary.roomId, + roomName = summary.name, + isDm = false, + markAsUnreadFeatureFlagEnabled = true, + hasNewContent = false, + ) + ) shownState.eventSink(RoomListEvents.HideContextMenu) val hiddenState = awaitItem() @@ -430,6 +469,41 @@ class RoomListPresenterTests { } } + @Test + fun `present - check that the room is marked as read with correct RR and as unread`() = runTest { + val room = FakeMatrixRoom() + val sessionPreferencesStore = InMemorySessionPreferencesStore() + val matrixClient = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter( + client = matrixClient, + coroutineScope = scope, + sessionPreferencesStore = sessionPreferencesStore, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(room.markAsReadCalls).isEmpty() + assertThat(room.markAsUnreadReadCallCount).isEqualTo(0) + initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID)) + assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) + assertThat(room.markAsUnreadReadCallCount).isEqualTo(0) + initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID)) + assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ)) + assertThat(room.markAsUnreadReadCallCount).isEqualTo(1) + // 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) + cancelAndIgnoreRemainingEvents() + scope.cancel() + } + } + private fun TestScope.createRoomListPresenter( client: MatrixClient = FakeMatrixClient(), sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), @@ -442,6 +516,7 @@ class RoomListPresenterTests { }, roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), encryptionService: EncryptionService = FakeEncryptionService(), + sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), coroutineScope: CoroutineScope, migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter( matrixClient = client, @@ -472,23 +547,6 @@ class RoomListPresenterTests { featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), ), migrationScreenPresenter = migrationScreenPresenter, + sessionPreferencesStore = sessionPreferencesStore, ) } - -private const val A_FORMATTED_DATE = "formatted_date" - -private val aRoomListRoomSummary = RoomListRoomSummary( - id = A_ROOM_ID.value, - roomId = A_ROOM_ID, - name = A_ROOM_NAME, - numberOfUnreadMentions = 1, - numberOfUnreadMessages = 2, - numberOfUnreadNotifications = 0, - timestamp = A_FORMATTED_DATE, - lastMessage = "", - avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), - isPlaceholder = false, - userDefinedNotificationMode = null, - hasRoomCall = false, - isDm = false, -) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt new file mode 100644 index 0000000000..f36299b1b7 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * 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 io.element.android.features.roomlist.impl.model + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import org.junit.Test + +class RoomListRoomSummaryTest { + @Test + fun `test default value`() { + val sut = createRoomListRoomSummary( + isMarkedUnread = false, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isFalse() + } + + @Test + fun `test muted room`() { + val sut = createRoomListRoomSummary( + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isFalse() + } + + @Test + fun `test muted room isMarkedUnread set to true`() { + val sut = createRoomListRoomSummary( + isMarkedUnread = true, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(sut.isHighlighted).isTrue() + assertThat(sut.hasNewContent).isTrue() + } + + @Test + fun `test muted room with unread message`() { + val sut = createRoomListRoomSummary( + numberOfUnreadNotifications = 1, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(sut.isHighlighted).isFalse() + assertThat(sut.hasNewContent).isTrue() + } + + @Test + fun `test isMarkedUnread set to true`() { + val sut = createRoomListRoomSummary( + isMarkedUnread = true, + ) + assertThat(sut.isHighlighted).isTrue() + assertThat(sut.hasNewContent).isTrue() + } +} + +internal fun createRoomListRoomSummary( + numberOfUnreadMentions: Int = 0, + numberOfUnreadMessages: Int = 0, + numberOfUnreadNotifications: Int = 0, + isMarkedUnread: Boolean = false, + userDefinedNotificationMode: RoomNotificationMode? = null, +) = RoomListRoomSummary( + id = A_ROOM_ID.value, + roomId = A_ROOM_ID, + name = A_ROOM_NAME, + numberOfUnreadMentions = numberOfUnreadMentions, + numberOfUnreadMessages = numberOfUnreadMessages, + numberOfUnreadNotifications = numberOfUnreadNotifications, + isMarkedUnread = isMarkedUnread, + timestamp = A_FORMATTED_DATE, + lastMessage = "", + avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), + isPlaceholder = false, + userDefinedNotificationMode = userDefinedNotificationMode, + hasRoomCall = false, + isDm = false, +) diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt index 47226c34d9..ca248f6bf8 100644 --- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt +++ b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt @@ -18,6 +18,8 @@ package io.element.android.libraries.dateformatter.test import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter +const val A_FORMATTED_DATE = "formatted_date" + class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter { private var format = "" fun givenFormat(format: String) { diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 61c155caac..3c56c81fae 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -75,4 +75,11 @@ enum class FeatureFlags( defaultValue = true, isFinished = false, ), + MarkAsUnread( + key = "feature.markAsUnread", + title = "Mark as unread", + description = "Allow user to mark a room as unread", + defaultValue = true, + isFinished = false, + ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 75b16d4e84..8462a33ba5 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -40,6 +40,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.PinUnlock -> true FeatureFlags.Mentions -> true FeatureFlags.SecureStorage -> true + FeatureFlags.MarkAsUnread -> false } } else { false 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 550bb7012a..051e0e419e 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 @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import kotlinx.coroutines.flow.Flow @@ -150,6 +151,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. + */ + suspend fun markAsRead(receiptType: ReceiptType?): Result + + /** + * Sets a flag on the room to indicate that the user has explicitly marked it as unread. + */ + suspend fun markAsUnread(): Result + /** * Share a location message in the room. * diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt index cc584f08bb..b8e6dbd84f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt @@ -43,6 +43,7 @@ data class RoomSummaryDetails( val numUnreadMessages: Int, val numUnreadMentions: Int, val numUnreadNotifications: Int, + val isMarkedUnread: Boolean, val inviter: RoomMember?, val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, 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 3fc7d65468..36b6cde936 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 @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.toProgressWatcher @@ -51,6 +52,7 @@ import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline +import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl @@ -423,6 +425,22 @@ class RustMatrixRoom( } } + override suspend fun markAsRead(receiptType: ReceiptType?): Result = withContext(roomDispatcher) { + runCatching { + if (receiptType != null) { + innerRoom.markAsReadAndSendReadReceipt(receiptType.toRustReceiptType()) + } else { + innerRoom.markAsRead() + } + } + } + + override suspend fun markAsUnread(): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.markAsUnread() + } + } + override suspend fun sendLocation( body: String, geoUri: String, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt index 1b3e900f17..cd3c86871b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt @@ -38,6 +38,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto numUnreadMentions = roomInfo.numUnreadMentions.toInt(), numUnreadMessages = roomInfo.numUnreadMessages.toInt(), numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(), + isMarkedUnread = roomInfo.isMarkedUnread, lastMessage = latestRoomMessage, inviter = roomInfo.inviter?.let(RoomMemberMapper::map), userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode), 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 c33a6d43b2..fdd0dbb6cb 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 @@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings @@ -374,6 +375,20 @@ class FakeMatrixRoom( return reportContentResult } + val markAsReadCalls = mutableListOf() + override suspend fun markAsRead(receiptType: ReceiptType?): Result { + markAsReadCalls.add(receiptType) + return Result.success(Unit) + } + + var markAsUnreadReadCallCount = 0 + private set + + override suspend fun markAsUnread(): Result { + markAsUnreadReadCallCount++ + return Result.success(Unit) + } + override suspend fun sendLocation( body: String, geoUri: String, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 2bd04894c0..4871dcb402 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -37,8 +37,8 @@ fun aRoomSummaryFilled( isDirect: Boolean = false, avatarUrl: String? = null, lastMessage: RoomMessage? = aRoomMessage(), - numUnreadMentions: Int = 1, - numUnreadMessages: Int = 2, + numUnreadMentions: Int = 0, + numUnreadMessages: Int = 0, notificationMode: RoomNotificationMode? = null, ) = RoomSummary.Filled( aRoomSummaryDetails( @@ -62,6 +62,7 @@ fun aRoomSummaryDetails( numUnreadMentions: Int = 0, numUnreadMessages: Int = 0, numUnreadNotifications: Int = 0, + isMarkedUnread: Boolean = false, notificationMode: RoomNotificationMode? = null, inviter: RoomMember? = null, canonicalAlias: String? = null, @@ -76,6 +77,7 @@ fun aRoomSummaryDetails( numUnreadMentions = numUnreadMentions, numUnreadMessages = numUnreadMessages, numUnreadNotifications = numUnreadNotifications, + isMarkedUnread = isMarkedUnread, userDefinedNotificationMode = notificationMode, inviter = inviter, canonicalAlias = canonicalAlias, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt index a4d9a479c2..bebe82dca2 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt @@ -116,6 +116,7 @@ fun aRoomSummaryDetails( numUnreadMentions: Int = 0, numUnreadMessages: Int = 0, numUnreadNotifications: Int = 0, + isMarkedUnread: Boolean = false, ) = RoomSummaryDetails( roomId = roomId, name = name, @@ -130,4 +131,5 @@ fun aRoomSummaryDetails( numUnreadMentions = numUnreadMentions, numUnreadMessages = numUnreadMessages, numUnreadNotifications = numUnreadNotifications, + isMarkedUnread = isMarkedUnread, ) diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index edf2527572..f9bfc63932 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(projects.libraries.network) implementation(projects.libraries.dateformatter.impl) implementation(projects.libraries.eventformatter.impl) + implementation(projects.libraries.preferences.impl) implementation(projects.libraries.indicator.impl) implementation(projects.features.invitelist.impl) implementation(projects.features.roomlist.impl) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index f87119293d..218dfdd697 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -45,6 +45,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -108,7 +109,12 @@ class RoomListScreen( migrationScreenPresenter = MigrationScreenPresenter( matrixClient = matrixClient, migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE)) - ) + ), + sessionPreferencesStore = DefaultSessionPreferencesStore( + context = context, + sessionId = matrixClient.sessionId, + sessionCoroutineScope = Singleton.appScope + ), ) @Composable diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png index d5a64e473f..111f5427d6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bd5f590fd30facfb007050c26aa567d568ab0d797a27986b6f88a23af76f9dc -size 14167 +oid sha256:547fe148039b4f0c4e84897af1b950d372ddb6aa5d0d79b53ca05fbc7fbdc11f +size 17388 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png index 1d1319d036..5585832f27 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e609d908d159cffdc2a17ac8944413dbb52e5cd1ebdd2bd30057dc9865749bd6 -size 13369 +oid sha256:2c603ba7cecfbb554b2c1c64bfba76f5c97d1c58c3adbbe69fb944592abca54a +size 16287 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png index 6e7ba7e325..1f28bb6439 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c7e78d4f8fd595922f4bdce5263bf9c5cf7094df5edb9a0e9d35fe270b21d23 -size 12541 +oid sha256:b142adcfc92ed9ddca5c8ec788c610b11296c128a577e5766ef1b10aa6a216cb +size 15550 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png index a6395dcbd7..f8de05de07 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:715154a104ef257a1865be35775cc7223e7adf3494863e0d24f1f1a8c7129920 -size 11888 +oid sha256:f898bbf9dd2cc11c594261f08682d8cebaefe1b9d8e526793c6f23a51d0f696b +size 14560