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)
This commit is contained in:
David Langley
2023-09-14 14:24:13 +01:00
parent 433c3554f8
commit 2a7a46841e
13 changed files with 145 additions and 12 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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<ImmutableList<RoomListRoomSummary>>(persistentListOf())
private val _filteredRooms = MutableStateFlow<ImmutableList<RoomListRoomSummary>>(persistentListOf())
@@ -59,13 +86,15 @@ class RoomListDataSource @Inject constructor(
private val diffCacheUpdater = DiffCacheUpdater<RoomSummary, RoomListRoomSummary>(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<ImmutableList<RoomListRoomSummary>> = _allRooms
val filteredRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _filteredRooms
private suspend fun replaceWith(roomSummaries: List<RoomSummary>) = withContext(coroutineDispatchers.computation) {
private suspend fun replaceWith(roomSummaries: List<RoomSummary>, notificationsEnabled: Boolean) = withContext(coroutineDispatchers.computation) {
lock.withLock {
diffCacheUpdater.updateWith(roomSummaries)
buildAndEmitAllRooms(roomSummaries)
buildAndEmitAllRooms(roomSummaries, notificationsEnabled)
}
}
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>) {
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, 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<RoomSummary>,
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
}

View File

@@ -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,
)

View File

@@ -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<RoomListRoomSummary> {
override val values: Sequence<RoomListRoomSummary>
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(

View File

@@ -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
}

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<group>
<clip-path
android:pathData="M0,0h16v16h-16z"/>
<path
android:pathData="M8,14.667C7.077,14.667 6.211,14.492 5.4,14.142C4.589,13.792 3.883,13.317 3.283,12.717C2.683,12.117 2.208,11.411 1.858,10.6C1.508,9.789 1.333,8.922 1.333,8C1.333,7.078 1.508,6.211 1.858,5.4C2.208,4.589 2.683,3.883 3.283,3.283C3.883,2.683 4.589,2.208 5.4,1.858C6.211,1.508 7.077,1.333 8,1.333C8.922,1.333 9.789,1.508 10.6,1.858C11.411,2.208 12.116,2.683 12.716,3.283C13.316,3.883 13.791,4.589 14.141,5.4C14.491,6.211 14.666,7.078 14.666,8V9.017C14.666,9.672 14.441,10.228 13.991,10.683C13.541,11.139 12.989,11.367 12.333,11.367C11.944,11.367 11.578,11.286 11.233,11.125C10.889,10.964 10.6,10.733 10.366,10.433C10.055,10.744 9.694,10.981 9.283,11.142C8.872,11.303 8.444,11.383 8,11.383C7.055,11.383 6.255,11.056 5.6,10.4C4.944,9.744 4.616,8.944 4.616,8C4.616,7.055 4.944,6.255 5.6,5.6C6.255,4.944 7.055,4.617 8,4.617C8.944,4.617 9.744,4.944 10.4,5.6C11.055,6.255 11.383,7.055 11.383,8V9.017C11.383,9.272 11.477,9.494 11.666,9.683C11.855,9.872 12.078,9.967 12.333,9.967C12.589,9.967 12.808,9.872 12.991,9.683C13.175,9.494 13.266,9.272 13.266,9.017V8C13.266,6.533 12.755,5.289 11.733,4.267C10.711,3.244 9.466,2.733 8,2.733C6.533,2.733 5.289,3.244 4.266,4.267C3.244,5.289 2.733,6.533 2.733,8C2.733,9.467 3.244,10.711 4.266,11.733C5.289,12.755 6.533,13.267 8,13.267H10.6C10.789,13.267 10.953,13.336 11.091,13.475C11.23,13.614 11.3,13.778 11.3,13.967C11.3,14.156 11.23,14.319 11.091,14.458C10.953,14.597 10.789,14.667 10.6,14.667H8ZM8,9.983C8.555,9.983 9.025,9.792 9.408,9.408C9.791,9.025 9.983,8.555 9.983,8C9.983,7.444 9.791,6.975 9.408,6.592C9.025,6.208 8.555,6.017 8,6.017C7.444,6.017 6.975,6.208 6.591,6.592C6.208,6.975 6.016,7.444 6.016,8C6.016,8.555 6.208,9.025 6.591,9.408C6.975,9.792 7.444,9.983 8,9.983Z"
android:fillColor="#A6ADB7"/>
</group>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="15dp"
android:viewportWidth="15"
android:viewportHeight="15">
<path
android:pathData="M1.905,0.491C1.515,0.1 0.882,0.1 0.491,0.491C0.101,0.881 0.101,1.515 0.491,1.905L3.152,4.566C3.033,4.951 2.969,5.359 2.969,5.781V8.703C2.969,9.477 2.539,9.992 2.032,10.293C1.689,10.499 1.25,10.783 1.25,11.127C1.25,11.522 1.5,11.788 2.015,11.788H7.266H10.374L13.095,14.509C13.486,14.9 14.119,14.9 14.509,14.509C14.9,14.119 14.9,13.486 14.509,13.095L1.905,0.491ZM11.563,8.703C11.563,8.779 11.567,8.852 11.575,8.923L4.886,2.234C5.346,1.921 5.864,1.693 6.411,1.576C6.408,1.546 6.407,1.515 6.407,1.484C6.407,1.01 6.791,0.625 7.266,0.625C7.741,0.625 8.125,1.01 8.125,1.484C8.125,1.515 8.124,1.546 8.121,1.576C10.02,1.984 11.563,3.713 11.563,5.781V8.703ZM6.054,12.656C6.003,12.794 5.977,12.931 5.977,13.086C5.977,13.799 6.553,14.375 7.266,14.375C7.979,14.375 8.555,13.799 8.555,13.086C8.555,12.931 8.521,12.794 8.478,12.656H6.054Z"
android:fillColor="#A6ADB7"
android:fillType="evenOdd"/>
</vector>

View File

@@ -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.
*/

View File

@@ -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,
)

View File

@@ -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),
)
}
}

View File

@@ -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<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
when (update) {
is RoomListEntriesUpdate.Append -> {

View File

@@ -107,6 +107,12 @@ class RustRoomListService(
}
}
override fun rebuildRoomSummaries() {
sessionCoroutineScope.launch {
allRoomsListProcessor.rebuildRoomSummaries()
}
}
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
innerRoomListService.syncIndicator()
.map { it.toSyncIndicator() }