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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
13
libraries/designsystem/src/main/res/drawable/ic_mention.xml
Normal file
13
libraries/designsystem/src/main/res/drawable/ic_mention.xml
Normal 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>
|
||||
10
libraries/designsystem/src/main/res/drawable/ic_mute.xml
Normal file
10
libraries/designsystem/src/main/res/drawable/ic_mute.xml
Normal 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>
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -107,6 +107,12 @@ class RustRoomListService(
|
||||
}
|
||||
}
|
||||
|
||||
override fun rebuildRoomSummaries() {
|
||||
sessionCoroutineScope.launch {
|
||||
allRoomsListProcessor.rebuildRoomSummaries()
|
||||
}
|
||||
}
|
||||
|
||||
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
|
||||
innerRoomListService.syncIndicator()
|
||||
.map { it.toSyncIndicator() }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -58,6 +58,10 @@ class FakeRoomListService : RoomListService {
|
||||
latestSlidingSyncRange = range
|
||||
}
|
||||
|
||||
override fun rebuildRoomSummaries() {
|
||||
|
||||
}
|
||||
|
||||
override fun allRooms(): RoomList {
|
||||
return SimpleRoomList(
|
||||
summaries = allRoomSummariesFlow,
|
||||
|
||||
@@ -78,6 +78,8 @@ class RoomListScreen(
|
||||
stateContentFormatter = StateContentFormatter(stringProvider),
|
||||
),
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
notificationSettingsService = matrixClient.notificationSettingsService(),
|
||||
appScope = Singleton.appScope
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user