diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index a6982f8201..251b5608b8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -33,6 +34,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -41,16 +45,20 @@ import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider import io.element.android.libraries.core.extensions.orEmpty +import io.element.android.libraries.designsystem.VectorIcons import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.unreadIndicator +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings internal val minHeight = 84.dp @@ -161,11 +169,39 @@ private fun RowScope.LastMessageAndIndicatorRow(room: RoomListRoomSummary) { maxLines = 2, overflow = TextOverflow.Ellipsis ) + // Unread - UnreadIndicatorAtom( - modifier = Modifier.padding(top = 3.dp), - isVisible = room.hasUnread, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + NotificationIcon(room) + if (room.hasUnread) { + UnreadIndicatorAtom( + modifier = Modifier.padding(top = 3.dp), + ) + } + } + +} + +@Composable +private fun NotificationIcon(room: RoomListRoomSummary) { + val tint = if(room.hasUnread) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary + when(room.notificationMode) { + null, RoomNotificationMode.ALL_MESSAGES -> return + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> + Icon( + contentDescription = stringResource(CommonStrings.screen_notification_settings_mode_mentions), + imageVector = ImageVector.vectorResource(VectorIcons.Mention), + tint = tint, + ) + RoomNotificationMode.MUTE -> + Icon( + contentDescription = stringResource(CommonStrings.common_mute), + imageVector = ImageVector.vectorResource(VectorIcons.Mute), + tint = tint, + ) + } } @Preview diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt index e44bcd6b6b..6a9c152af6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt @@ -27,28 +27,37 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds class RoomListDataSource @Inject constructor( private val roomListService: RoomListService, private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter, private val coroutineDispatchers: CoroutineDispatchers, + private val notificationSettingsService: NotificationSettingsService, + private val appScope: CoroutineScope, ) { + init { + observeNotificationSettings() + } private val _filter = MutableStateFlow("") private val _allRooms = MutableStateFlow>(persistentListOf()) @@ -92,6 +101,16 @@ class RoomListDataSource @Inject constructor( val allRooms: StateFlow> = _allRooms val filteredRooms: StateFlow> = _filteredRooms + @OptIn(FlowPreview::class) + private fun observeNotificationSettings() { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + roomListService.rebuildRoomSummaries() + } + .launchIn(appScope) + } + private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) { lock.withLock { diffCacheUpdater.updateWith(roomSummaries) @@ -120,10 +139,7 @@ class RoomListDataSource @Inject constructor( } } - private fun buildAndCacheItem( - roomSummaries: List, - index: Int - ): RoomListRoomSummary? { + private fun buildAndCacheItem(roomSummaries: List, index: Int): RoomListRoomSummary? { val roomListRoomSummary = when (val roomSummary = roomSummaries.getOrNull(index)) { is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Filled -> { @@ -144,10 +160,12 @@ class RoomListDataSource @Inject constructor( roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect) }.orEmpty(), avatarData = avatarData, + notificationMode = roomSummary.details.notificationMode, ) } null -> null } + diffCache[index] = roomListRoomSummary return roomListRoomSummary } 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 8ba6c26c0f..bb3405c2d3 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 @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable 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.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode @Immutable data class RoomListRoomSummary constructor( @@ -31,4 +32,5 @@ data class RoomListRoomSummary constructor( val lastMessage: CharSequence? = null, val avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem), val isPlaceholder: Boolean = false, + val notificationMode: RoomNotificationMode? = null, ) 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 cd6ca21106..f2db8a376c 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 @@ -20,14 +20,16 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode open class RoomListRoomSummaryProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomListRoomSummary(), aRoomListRoomSummary().copy(lastMessage = null), - aRoomListRoomSummary().copy(hasUnread = true), - aRoomListRoomSummary().copy(timestamp = "88:88"), + aRoomListRoomSummary().copy(hasUnread = true, notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY), + aRoomListRoomSummary().copy(timestamp = "88:88", notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY), + aRoomListRoomSummary().copy(timestamp = "88:88", notificationMode = RoomNotificationMode.MUTE), aRoomListRoomSummary().copy(timestamp = "88:88", hasUnread = true), aRoomListRoomSummary().copy(isPlaceholder = true, timestamp = "88:88"), aRoomListRoomSummary().copy( 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 b7bfd3ecb4..db2f7027c4 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 @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.RoomNotificationMode 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 @@ -47,12 +48,16 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID 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.notificationsettings.FakeNotificationSettingsService 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 import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -66,7 +71,8 @@ class RoomListPresenterTests { @Test fun `present - should start with no user and then load user with success`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -77,6 +83,7 @@ class RoomListPresenterTests { Truth.assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID) Truth.assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME) Truth.assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL) + scope.cancel() } } @@ -86,7 +93,8 @@ class RoomListPresenterTests { userDisplayName = Result.failure(AN_EXCEPTION), userAvatarURLString = Result.failure(AN_EXCEPTION), ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -94,12 +102,14 @@ class RoomListPresenterTests { Truth.assertThat(initialState.matrixUser).isNull() val withUserState = awaitItem() Truth.assertThat(withUserState.matrixUser).isNotNull() + scope.cancel() } } @Test fun `present - should filter room with success`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -109,8 +119,8 @@ class RoomListPresenterTests { withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) val withFilterState = awaitItem() Truth.assertThat(withFilterState.filter).isEqualTo("t") - cancelAndIgnoreRemainingEvents() + scope.cancel() } } @@ -120,7 +130,8 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -133,6 +144,7 @@ class RoomListPresenterTests { Truth.assertThat(withRoomState.roomList.size).isEqualTo(1) Truth.assertThat(withRoomState.roomList.first()) .isEqualTo(aRoomListRoomSummary) + scope.cancel() } } @@ -142,7 +154,8 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -160,6 +173,7 @@ class RoomListPresenterTests { val withNotFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 0 }.last() Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo("tada") Truth.assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty() + scope.cancel() } } @@ -169,7 +183,8 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val presenter = createRoomListPresenter(matrixClient) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -200,6 +215,7 @@ class RoomListPresenterTests { Truth.assertThat(roomListService.latestSlidingSyncRange) .isEqualTo(IntRange(129, 279)) cancelAndIgnoreRemainingEvents() + scope.cancel() } } @@ -209,12 +225,14 @@ class RoomListPresenterTests { val matrixClient = FakeMatrixClient( roomListService = roomListService, ) + val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( client = matrixClient, sessionVerificationService = FakeSessionVerificationService().apply { givenIsReady(true) givenVerifiedStatus(SessionVerifiedStatus.NotVerified) }, + coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -224,6 +242,7 @@ class RoomListPresenterTests { eventSink(RoomListEvents.DismissRequestVerificationPrompt) Truth.assertThat(awaitItem().displayVerificationPrompt).isFalse() + scope.cancel() } } @@ -231,7 +250,8 @@ class RoomListPresenterTests { fun `present - sets invite state`() = runTest { val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites) val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow) - val presenter = createRoomListPresenter(inviteStateDataSource = inviteStateDataSource) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(inviteStateDataSource = inviteStateDataSource, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -246,12 +266,14 @@ class RoomListPresenterTests { inviteStateFlow.value = InvitesState.NoInvites Truth.assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites) + scope.cancel() } } @Test fun `present - show context menu`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -264,12 +286,14 @@ class RoomListPresenterTests { val shownState = awaitItem() Truth.assertThat(shownState.contextMenu) .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name)) + scope.cancel() } } @Test fun `present - hide context menu`() = runTest { - val presenter = createRoomListPresenter() + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -286,13 +310,15 @@ class RoomListPresenterTests { val hiddenState = awaitItem() Truth.assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden) + scope.cancel() } } @Test fun `present - leave room calls into leave room presenter`() = runTest { val leaveRoomPresenter = LeaveRoomPresenterFake() - val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -300,6 +326,35 @@ class RoomListPresenterTests { initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID)) Truth.assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) cancelAndIgnoreRemainingEvents() + scope.cancel() + } + } + + @Test + fun `present - change in notification settings updates the summary for decorations`() = runTest { + val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + val notificationSettingsService = FakeNotificationSettingsService() + val roomListService = FakeRoomListService() + roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode))) + val matrixClient = FakeMatrixClient( + roomListService = roomListService, + notificationSettingsService = notificationSettingsService + ) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter(client = matrixClient , coroutineScope = scope) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode) + + val updatedState = consumeItemsUntilPredicate { state -> + state.roomList.any { it.id == A_ROOM_ID.value && it.notificationMode == userDefinedMode } + }.last() + + val room = updatedState.roomList.find { it.id == A_ROOM_ID.value } + Truth.assertThat(room?.notificationMode).isEqualTo(userDefinedMode) + cancelAndIgnoreRemainingEvents() + scope.cancel() } } @@ -313,7 +368,8 @@ class RoomListPresenterTests { lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) }, - roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter() + roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), + coroutineScope: CoroutineScope = this, ) = RoomListPresenter( client = client, sessionVerificationService = sessionVerificationService, @@ -325,7 +381,9 @@ class RoomListPresenterTests { client.roomListService, lastMessageTimestampFormatter, roomLastMessageFormatter, - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(), + notificationSettingsService = client.notificationSettingsService(), + appScope = coroutineScope ) ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 5f9764d8f9..919c505439 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -41,6 +41,8 @@ object VectorIcons { val Quote = R.drawable.ic_quote val Strikethrough = R.drawable.ic_strikethrough val Underline = R.drawable.ic_underline + val Mention = R.drawable.ic_mention + val Mute = R.drawable.ic_mute val ThreadDecoration = R.drawable.ic_thread_decoration val Plus = R.drawable.ic_plus val Cancel = R.drawable.ic_cancel diff --git a/libraries/designsystem/src/main/res/drawable/ic_mention.xml b/libraries/designsystem/src/main/res/drawable/ic_mention.xml new file mode 100644 index 0000000000..37f70481e5 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_mention.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_mute.xml b/libraries/designsystem/src/main/res/drawable/ic_mute.xml new file mode 100644 index 0000000000..ea6f842696 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_mute.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index de7f1f3918..8b85b3fe38 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -54,6 +54,11 @@ interface RoomListService { */ fun updateAllRoomsVisibleRange(range: IntRange) + /** + * Rebuild the room summaries, required when we know some data may have changed. (E.g. room notification settings) + */ + fun rebuildRoomSummaries() + /** * The sync indicator as a flow. */ 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 87cf2139d6..4638fc6e03 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.roomlist import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.message.RoomMessage sealed interface RoomSummary { @@ -42,4 +43,5 @@ data class RoomSummaryDetails( val lastMessageTimestamp: Long?, val unreadNotificationCount: Int, val inviter: RoomMember? = null, + val notificationMode: RoomNotificationMode? = null, ) 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 d33e2f58fc..a9d46296db 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails +import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper import io.element.android.libraries.matrix.impl.room.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory import org.matrix.rustcomponents.sdk.RoomInfo @@ -39,6 +40,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto lastMessage = latestRoomMessage, lastMessageTimestamp = latestRoomMessage?.originServerTs, inviter = roomInfo.inviter?.let(RoomMemberMapper::map), + notificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode), ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index 321a17d809..99b9c17968 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate @@ -59,6 +60,17 @@ class RoomSummaryListProcessor( } } + suspend fun rebuildRoomSummaries() { + updateRoomSummaries { + forEachIndexed { i, summary -> + this[i] = when(summary) { + is RoomSummary.Empty -> summary + is RoomSummary.Filled -> buildAndCacheRoomSummaryForIdentifier(summary.identifier()) + } + } + } + } + private fun MutableList.applyUpdate(update: RoomListEntriesUpdate) { when (update) { is RoomListEntriesUpdate.Append -> { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index a2de2be89b..347e53aa0c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -107,6 +107,12 @@ class RustRoomListService( } } + override fun rebuildRoomSummaries() { + sessionCoroutineScope.launch { + allRoomsListProcessor.rebuildRoomSummaries() + } + } + override val syncIndicator: StateFlow = innerRoomListService.syncIndicator() .map { it.toSyncIndicator() } 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 12b0325da5..d3b4dbc577 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 @@ -20,6 +20,7 @@ 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.TransactionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.room.message.RoomMessage @@ -48,6 +49,7 @@ fun aRoomSummaryFilled( lastMessage: RoomMessage? = aRoomMessage(), lastMessageTimestamp: Long? = null, unreadNotificationCount: Int = 2, + notificationMode: RoomNotificationMode? = null, ) = RoomSummary.Filled( aRoomSummaryDetail( roomId = roomId, @@ -57,6 +59,7 @@ fun aRoomSummaryFilled( lastMessage = lastMessage, lastMessageTimestamp = lastMessageTimestamp, unreadNotificationCount = unreadNotificationCount, + notificationMode = notificationMode, ) ) @@ -68,6 +71,7 @@ fun aRoomSummaryDetail( lastMessage: RoomMessage? = aRoomMessage(), lastMessageTimestamp: Long? = null, unreadNotificationCount: Int = 2, + notificationMode: RoomNotificationMode? = null, ) = RoomSummaryDetails( roomId = roomId, name = name, @@ -76,6 +80,7 @@ fun aRoomSummaryDetail( lastMessage = lastMessage, lastMessageTimestamp = lastMessageTimestamp, unreadNotificationCount = unreadNotificationCount, + notificationMode = notificationMode ) fun aRoomMessage( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt index 7e0f3e8891..75a91508d0 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt @@ -58,6 +58,10 @@ class FakeRoomListService : RoomListService { latestSlidingSyncRange = range } + override fun rebuildRoomSummaries() { + + } + override fun allRooms(): RoomList { return SimpleRoomList( summaries = allRoomSummariesFlow, 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 faaccc9b8e..425888f003 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 @@ -78,6 +78,8 @@ class RoomListScreen( stateContentFormatter = StateContentFormatter(stringProvider), ), coroutineDispatchers = coroutineDispatchers, + notificationSettingsService = matrixClient.notificationSettingsService(), + appScope = Singleton.appScope ) ) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png index 5d673a1cff..4ad70e1e7d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e710857b4a1c3da12ceb51e7400229d0fd7f42d6d6158a07e2c2814c728abfdc -size 12229 +oid sha256:a4451ed5c83109d1a36d262b9d6cca1df4f65998fa2933e77b30ffeec2a1688e +size 12980 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png index d0ec7b4cb1..7d20bddd45 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db014e76a88015053b1be458ac758dde99f8cf01a7a78850c0bf6ff4db27dcbc -size 13173 +oid sha256:6ca678d3cb26b3224c1fe44169eebd0a96ddcc344e2d9d45ff84c41116c03a0e +size 13829 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png index 606a183dce..b27b7924e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d062feda6fc808aa87df5a5f4b3229153f7e50925e3c55a8c8a99a583212857 -size 13458 +oid sha256:8618c1af337c98cbd812632fc6dbb468b64a3b53e2141a7073a5ea52656ddaa7 +size 13611 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png index 805e00c2b7..606a183dce 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cd7f6969aa48fdde75e1ad046d595882ebdadfc310cd6b6753f43895aec7b99 -size 6281 +oid sha256:4d062feda6fc808aa87df5a5f4b3229153f7e50925e3c55a8c8a99a583212857 +size 13458 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png index 7078257343..805e00c2b7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79b24ca5c6426aa966024d79bdbbc4a612169037cf6bda7496299ea298fa7517 -size 21951 +oid sha256:7cd7f6969aa48fdde75e1ad046d595882ebdadfc310cd6b6753f43895aec7b99 +size 6281 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7078257343 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowDark_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79b24ca5c6426aa966024d79bdbbc4a612169037cf6bda7496299ea298fa7517 +size 21951 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png index e471eae254..6b4a39b0f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c9b33e480bfad9c55d55e25cef2a0ab1f25235382e1b53c93107a75e95fa90e -size 11840 +oid sha256:496059d16235c52562acf31c209d917b3eff6142324e07476e01a732d9aaf816 +size 12661 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png index 582d30fdfc..d08e94a349 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:824e708f277ea66a161f17cb07a3070e03ddcfc326b9dd73d0ad6b7e62da32c1 -size 12807 +oid sha256:6f6bfe205fe096afe6d976ed242d823d02d06a01b8f81e5feca7b9a71d7b5767 +size 13543 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png index 0de6b64bec..4a51acc8ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39b7b8354b98bca791698ad65f4e21619136f446b2fd050c302bdd27ce138a4a -size 13263 +oid sha256:8337468ce85f5f1aa9fec867d9037b71c7460fa823c7f6fdcbf0efe48ad97d40 +size 13293 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png index 6bfb7eeabe..0de6b64bec 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c978bc799ab79290f89568de692d3ace219a4192ec9706fef63fe977a853f74d -size 6046 +oid sha256:39b7b8354b98bca791698ad65f4e21619136f446b2fd050c302bdd27ce138a4a +size 13263 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png index fcf8b20fdc..6bfb7eeabe 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64e7ca21b0213413ed45d0640295ceb2764b01b2bce20a01518e55e620925086 -size 22193 +oid sha256:c978bc799ab79290f89568de692d3ace219a4192ec9706fef63fe977a853f74d +size 6046 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fcf8b20fdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRowLight_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64e7ca21b0213413ed45d0640295ceb2764b01b2bce20a01518e55e620925086 +size 22193