From 2a7a46841ea4599001cdd9e4e97188941392160b Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 14 Sep 2023 14:24:13 +0100 Subject: [PATCH] Show a room list decoration for notification setting applied - Add the UI - Rebuild room summaries when push rules change or when user disables notifications(hide them all) --- features/roomlist/impl/build.gradle.kts | 1 + .../impl/components/RoomSummaryRow.kt | 46 +++++++++++++++-- .../impl/datasource/RoomListDataSource.kt | 50 ++++++++++++++++--- .../impl/model/RoomListRoomSummary.kt | 2 + .../impl/model/RoomListRoomSummaryProvider.kt | 6 ++- .../libraries/designsystem/VectorIcons.kt | 2 + .../src/main/res/drawable/ic_mention.xml | 13 +++++ .../src/main/res/drawable/ic_mute.xml | 10 ++++ .../matrix/api/roomlist/RoomListService.kt | 5 ++ .../matrix/api/roomlist/RoomSummary.kt | 2 + .../roomlist/RoomSummaryDetailsFactory.kt | 2 + .../impl/roomlist/RoomSummaryListProcessor.kt | 12 +++++ .../impl/roomlist/RustRoomListService.kt | 6 +++ 13 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 libraries/designsystem/src/main/res/drawable/ic_mention.xml create mode 100644 libraries/designsystem/src/main/res/drawable/ic_mute.xml diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index f5a08ba860..3a3639de98 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.eventformatter.api) implementation(projects.libraries.deeplink) + implementation(projects.libraries.pushstore.api) implementation(projects.features.invitelist.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.leaveroom.api) 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 aab174c6a4..61826dc2b2 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,14 +19,17 @@ 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 import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -38,6 +41,9 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape +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 @@ -48,16 +54,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 @@ -168,11 +178,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, + ) + } } val TextPlaceholderShape = PercentRectangleSizeShape(0.5f) 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..e0b20aada9 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 @@ -26,30 +26,57 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat 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.MatrixClient 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.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.pushstore.api.UserPushStoreFactory 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.distinctUntilChanged +import kotlinx.coroutines.flow.first 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, + notificationSettingsService: NotificationSettingsService, + appScope: CoroutineScope, + userPushStoreFactory: UserPushStoreFactory, + matrixClient: MatrixClient, ) { + init { + notificationSettingsService.notificationSettingsChangeFlow + .debounce(0.5.seconds) + .onEach { + roomListService.rebuildRoomSummaries() + } + .launchIn(appScope) + val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) + userPushStore.getNotificationEnabledForDevice().distinctUntilChanged() + .onEach { + roomListService.rebuildRoomSummaries() + } + .launchIn(appScope) + } private val _filter = MutableStateFlow("") private val _allRooms = MutableStateFlow>(persistentListOf()) private val _filteredRooms = MutableStateFlow>(persistentListOf()) @@ -59,13 +86,15 @@ class RoomListDataSource @Inject constructor( private val diffCacheUpdater = DiffCacheUpdater(diffCache = diffCache, detectMoves = true) { old, new -> old?.identifier() == new?.identifier() } + private val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) fun launchIn(coroutineScope: CoroutineScope) { + roomListService .allRooms() .summaries .onEach { roomSummaries -> - replaceWith(roomSummaries) + replaceWith(roomSummaries, userPushStore.getNotificationEnabledForDevice().first()) } .launchIn(coroutineScope) @@ -92,14 +121,14 @@ class RoomListDataSource @Inject constructor( val allRooms: StateFlow> = _allRooms val filteredRooms: StateFlow> = _filteredRooms - private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) { + private suspend fun replaceWith(roomSummaries: List, notificationsEnabled: Boolean) = withContext(coroutineDispatchers.computation) { lock.withLock { diffCacheUpdater.updateWith(roomSummaries) - buildAndEmitAllRooms(roomSummaries) + buildAndEmitAllRooms(roomSummaries, notificationsEnabled) } } - private suspend fun buildAndEmitAllRooms(roomSummaries: List) { + private suspend fun buildAndEmitAllRooms(roomSummaries: List, notificationsEnabled: Boolean) { if (diffCache.isEmpty()) { _allRooms.emit( RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList() @@ -109,7 +138,7 @@ class RoomListDataSource @Inject constructor( for (index in diffCache.indices()) { val cacheItem = diffCache.get(index) if (cacheItem == null) { - buildAndCacheItem(roomSummaries, index)?.also { timelineItemState -> + buildAndCacheItem(roomSummaries, index, notificationsEnabled)?.also { timelineItemState -> roomListRoomSummaries.add(timelineItemState) } } else { @@ -122,11 +151,18 @@ class RoomListDataSource @Inject constructor( private fun buildAndCacheItem( roomSummaries: List, - index: Int + index: Int, + notificationsEnabled: Boolean, ): RoomListRoomSummary? { val roomListRoomSummary = when (val roomSummary = roomSummaries.getOrNull(index)) { is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Filled -> { + // Only show a decoration if notifications are enabled and the mode is not ALL_MESSAGES + val notificationMode = if (roomSummary.details.notificationMode == RoomNotificationMode.ALL_MESSAGES || !notificationsEnabled) { + null + } else { + roomSummary.details.notificationMode + } val avatarData = AvatarData( id = roomSummary.identifier(), name = roomSummary.details.name, @@ -144,10 +180,12 @@ class RoomListDataSource @Inject constructor( roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect) }.orEmpty(), avatarData = avatarData, + notificationMode = 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/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 20ebb85615..229605e3ea 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,4 +41,6 @@ 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 } 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..f9f63ab271 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.notificationMode?.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 03b58ed2ce..9ae10db12d 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 suspend 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() }