Merge pull request #1330 from vector-im/dla/feature/room_list_decoration

Show a room list decoration for notification setting applied
This commit is contained in:
David Langley
2023-09-18 17:27:54 +01:00
committed by GitHub
28 changed files with 228 additions and 43 deletions

View File

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

View File

@@ -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<ImmutableList<RoomListRoomSummary>>(persistentListOf())
@@ -92,6 +101,16 @@ class RoomListDataSource @Inject constructor(
val allRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
val filteredRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _filteredRooms
@OptIn(FlowPreview::class)
private fun observeNotificationSettings() {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
roomListService.rebuildRoomSummaries()
}
.launchIn(appScope)
}
private suspend fun replaceWith(roomSummaries: List<RoomSummary>) = withContext(coroutineDispatchers.computation) {
lock.withLock {
diffCacheUpdater.updateWith(roomSummaries)
@@ -120,10 +139,7 @@ class RoomListDataSource @Inject constructor(
}
}
private fun buildAndCacheItem(
roomSummaries: List<RoomSummary>,
index: Int
): RoomListRoomSummary? {
private fun buildAndCacheItem(roomSummaries: List<RoomSummary>, 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
}

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

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

View File

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

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.userDefinedNotificationMode?.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 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() }

View File

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

View File

@@ -58,6 +58,10 @@ class FakeRoomListService : RoomListService {
latestSlidingSyncRange = range
}
override fun rebuildRoomSummaries() {
}
override fun allRooms(): RoomList {
return SimpleRoomList(
summaries = allRoomSummariesFlow,

View File

@@ -78,6 +78,8 @@ class RoomListScreen(
stateContentFormatter = StateContentFormatter(stringProvider),
),
coroutineDispatchers = coroutineDispatchers,
notificationSettingsService = matrixClient.notificationSettingsService(),
appScope = Singleton.appScope
)
)