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 3406b8a85f
commit ed1949aa51
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(