RoomList : refactor how we manage state so it's easier to reason about.

This commit is contained in:
ganfra
2024-03-09 16:53:48 +01:00
parent 39ec6aae53
commit c03dc07dcf
7 changed files with 368 additions and 169 deletions

View File

@@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -26,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
@@ -52,6 +54,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.timeline.ReceiptType
@@ -60,6 +63,7 @@ import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
@@ -100,11 +104,7 @@ class RoomListPresenter @Inject constructor(
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
mutableStateOf(null)
}
val roomList by produceState(initialValue = AsyncData.Loading()) {
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
}
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
val filtersState = filtersPresenter.present()
val searchState = searchPresenter.present()
@@ -113,28 +113,7 @@ class RoomListPresenter @Inject constructor(
initialLoad(matrixUser)
}
val isMigrating = migrationScreenPresenter.present().isMigrating
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
val isLastDevice by encryptionService.isLastDevice.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val syncState by syncService.syncState.collectAsState()
val securityBannerState by remember {
derivedStateOf {
when {
securityBannerDismissed -> SecurityBannerState.None
canVerifySession -> if (isLastDevice) {
SecurityBannerState.RecoveryKeyConfirmation
} else {
SecurityBannerState.SessionVerification
}
recoveryState == RecoveryState.INCOMPLETE &&
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
else -> SecurityBannerState.None
}
}
}
// Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
@@ -162,19 +141,18 @@ class RoomListPresenter @Inject constructor(
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val contentState = roomListContentState(securityBannerDismissed)
return RoomListState(
matrixUser = matrixUser.value,
showAvatarIndicator = showAvatarIndicator,
roomList = roomList,
securityBannerState = securityBannerState,
snackbarMessage = snackbarMessage,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
invitesState = inviteStateDataSource.inviteState(),
contextMenu = contextMenu.value,
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
displayMigrationStatus = isMigrating,
contentState = contentState,
eventSink = ::handleEvents,
)
}
@@ -183,6 +161,70 @@ class RoomListPresenter @Inject constructor(
matrixUser.value = client.getCurrentUser()
}
@Composable
private fun securityBannerState(
securityBannerDismissed: Boolean,
): State<SecurityBannerState> {
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
val isLastDevice by encryptionService.isLastDevice.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val syncState by syncService.syncState.collectAsState()
return remember {
derivedStateOf {
when {
currentSecurityBannerDismissed -> SecurityBannerState.None
canVerifySession -> if (isLastDevice) {
SecurityBannerState.RecoveryKeyConfirmation
} else {
SecurityBannerState.SessionVerification
}
recoveryState == RecoveryState.INCOMPLETE &&
syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation
else -> SecurityBannerState.None
}
}
}
}
@Composable
private fun roomListContentState(
securityBannerDismissed: Boolean,
): RoomListContentState {
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
}
val loadingState by roomListDataSource.loadingState.collectAsState()
val showMigration = migrationScreenPresenter.present().isMigrating
val showSkeleton by remember {
derivedStateOf {
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
}
}
val showEmpty by remember {
derivedStateOf {
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
}
}
return when {
showMigration -> RoomListContentState.Migration
showSkeleton -> RoomListContentState.Skeleton(count = 16)
showEmpty -> {
val invitesState = inviteStateDataSource.inviteState()
RoomListContentState.Empty(invitesState)
}
else -> {
val invitesState = inviteStateDataSource.inviteState()
val securityBannerState by securityBannerState(securityBannerDismissed)
RoomListContentState.Rooms(
invitesState = invitesState,
securityBannerState = securityBannerState,
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList()
)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
val initialState = RoomListState.ContextMenu.Shown(

View File

@@ -21,7 +21,6 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -31,20 +30,17 @@ import kotlinx.collections.immutable.ImmutableList
data class RoomListState(
val matrixUser: MatrixUser?,
val showAvatarIndicator: Boolean,
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
val securityBannerState: SecurityBannerState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val invitesState: InvitesState,
val contextMenu: ContextMenu,
val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState,
val searchState: RoomListSearchState,
val displayMigrationStatus: Boolean,
val contentState: RoomListContentState,
val eventSink: (RoomListEvents) -> Unit,
) {
val displayFilters = filtersState.isFeatureEnabled && !displayMigrationStatus
val displayEmptyState = roomList is AsyncData.Success && roomList.data.isEmpty()
val displayFilters = filtersState.isFeatureEnabled && contentState is RoomListContentState.Rooms
val displayActions = contentState !is RoomListContentState.Migration
sealed interface ContextMenu {
data object Hidden : ContextMenu
@@ -70,3 +66,14 @@ enum class SecurityBannerState {
SessionVerification,
RecoveryKeyConfirmation,
}
sealed interface RoomListContentState {
data object Migration : RoomListContentState
data class Skeleton(val count: Int) : RoomListContentState
data class Empty(val invitesState: InvitesState) : RoomListContentState
data class Rooms(
val invitesState: InvitesState,
val securityBannerState: SecurityBannerState,
val summaries: ImmutableList<RoomListRoomSummary>,
) : RoomListContentState
}

View File

@@ -19,7 +19,6 @@ package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
@@ -36,6 +35,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState>
@@ -50,7 +50,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState(securityBannerState = SecurityBannerState.SessionVerification),
aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
aRoomListState(roomList = AsyncData.Success(persistentListOf())),
aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
//aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
aRoomListState(matrixUser = null, displayMigrationStatus = true),
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
@@ -74,16 +74,17 @@ internal fun aRoomListState(
) = RoomListState(
matrixUser = matrixUser,
showAvatarIndicator = showAvatarIndicator,
roomList = roomList,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,
securityBannerState = securityBannerState,
invitesState = invitesState,
contextMenu = contextMenu,
leaveRoomState = leaveRoomState,
searchState = searchState,
filtersState = filtersState,
displayMigrationStatus = displayMigrationStatus,
searchState = searchState,
contentState = RoomListContentState.Rooms(
invitesState = invitesState,
securityBannerState = securityBannerState,
summaries = roomList.dataOrNull().orEmpty().toPersistentList(),
),
eventSink = eventSink,
)

View File

@@ -53,6 +53,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.features.roomlist.impl.components.ConfirmRecoveryKeyBanner
import io.element.android.features.roomlist.impl.components.RequestVerificationHeader
import io.element.android.features.roomlist.impl.components.RoomListContentView
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
@@ -108,7 +109,7 @@ fun RoomListView(
LeaveRoomView(state = state.leaveRoomState)
RoomListContent(
RoomListScaffold(
modifier = Modifier.padding(top = topPadding),
state = state,
onVerifyClicked = onVerifyClicked,
@@ -135,43 +136,9 @@ fun RoomListView(
}
}
@Composable
private fun EmptyRoomListView(
onCreateRoomClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.screen_roomlist_empty_title),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.screen_roomlist_empty_message),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
Button(
text = stringResource(CommonStrings.action_start_chat),
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
onClick = onCreateRoomClicked,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomListContent(
private fun RoomListScaffold(
state: RoomListState,
onVerifyClicked: () -> Unit,
onConfirmRecoveryKeyClicked: () -> Unit,
@@ -188,26 +155,7 @@ private fun RoomListContent(
}
val appBarState = rememberTopAppBarState()
val lazyListState = rememberLazyListState()
val visibleRange by remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
val size = layoutInfo.visibleItemsInfo.size
firstItemIndex until firstItemIndex + size
}
}
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
return super.onPostFling(consumed, available)
}
}
}
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
Scaffold(
@@ -222,7 +170,7 @@ private fun RoomListContent(
onMenuActionClicked = onMenuActionClicked,
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
displayMenuItems = !state.displayMigrationStatus,
displayMenuItems = !state.displayActions,
)
if (state.displayFilters) {
RoomListFiltersView(state = state.filtersState)
@@ -230,69 +178,22 @@ private fun RoomListContent(
}
},
content = { padding ->
LazyColumn(
RoomListContentView(
contentState = state.contentState,
eventSink = state.eventSink,
onVerifyClicked = onVerifyClicked,
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
onRoomClicked = ::onRoomClicked,
onRoomLongClicked = onRoomLongClicked,
onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.nestedScroll(nestedScrollConnection),
state = lazyListState,
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
contentPadding = PaddingValues(bottom = 80.dp)
) {
when {
state.displayEmptyState -> Unit
state.securityBannerState == SecurityBannerState.SessionVerification -> {
item {
RequestVerificationHeader(
onVerifyClicked = onVerifyClicked,
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
)
}
}
state.securityBannerState == SecurityBannerState.RecoveryKeyConfirmation -> {
item {
ConfirmRecoveryKeyBanner(
onContinueClicked = onConfirmRecoveryKeyClicked,
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
)
}
}
}
if (state.invitesState != InvitesState.NoInvites) {
item {
InvitesEntryPointView(onInvitesClicked, state.invitesState)
}
}
val roomList = state.roomList.dataOrNull().orEmpty()
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
// is moved to the top of the list.
itemsIndexed(
items = roomList,
contentType = { _, room -> room.contentType() },
) { index, room ->
RoomSummaryRow(
room = room,
onClick = ::onRoomClicked,
onLongClick = onRoomLongClicked,
)
if (index != roomList.lastIndex) {
HorizontalDivider()
}
}
}
if (state.displayEmptyState) {
if (state.filtersState.hasAnyFilterSelected) {
// TODO add empty state for filtered rooms
} else {
EmptyRoomListView(onCreateRoomClicked)
}
}
MigrationScreenView(isMigrating = state.displayMigrationStatus)
)
},
floatingActionButton = {
if (!state.displayMigrationStatus) {
if (state.displayActions) {
FloatingActionButton(
// FIXME align on Design system theme
containerColor = MaterialTheme.colorScheme.primary,

View File

@@ -0,0 +1,255 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomlist.impl.InvitesEntryPointView
import io.element.android.features.roomlist.impl.InvitesState
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListContentState
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.SecurityBannerState
import io.element.android.features.roomlist.impl.contentType
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomListContentView(
contentState: RoomListContentState,
eventSink: (RoomListEvents) -> Unit,
onVerifyClicked: () -> Unit,
onConfirmRecoveryKeyClicked: () -> Unit,
onRoomClicked: (RoomListRoomSummary) -> Unit,
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
onCreateRoomClicked: () -> Unit,
onInvitesClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
when (contentState) {
is RoomListContentState.Migration -> {
MigrationScreenView(isMigrating = true)
}
is RoomListContentState.Skeleton -> {
SkeletonView(
count = contentState.count,
)
}
is RoomListContentState.Empty -> {
EmptyView(
state = contentState,
onInvitesClicked = onInvitesClicked,
onCreateRoomClicked = onCreateRoomClicked,
)
}
is RoomListContentState.Rooms -> {
RoomsView(
state = contentState,
eventSink = eventSink,
onVerifyClicked = onVerifyClicked,
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
onRoomClicked = onRoomClicked,
onRoomLongClicked = onRoomLongClicked,
onInvitesClicked = onInvitesClicked,
)
}
}
}
}
@Composable
private fun SkeletonView(count: Int, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
repeat(count) { index ->
item {
RoomSummaryPlaceholderRow()
if (index != count - 1) {
HorizontalDivider()
}
}
}
}
}
@Composable
private fun EmptyView(
state: RoomListContentState.Empty,
onCreateRoomClicked: () -> Unit,
onInvitesClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
) {
if (state.invitesState != InvitesState.NoInvites) {
InvitesEntryPointView(onInvitesClicked, state.invitesState)
}
EmptyScaffold(
title = {
Text(
text = stringResource(R.string.screen_roomlist_empty_title),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
},
subtitle = {
Text(
text = stringResource(R.string.screen_roomlist_empty_message),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
},
action = {
Button(
text = stringResource(CommonStrings.action_start_chat),
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
onClick = onCreateRoomClicked,
)
},
modifier = Modifier,
)
}
}
@Composable
private fun RoomsView(
state: RoomListContentState.Rooms,
eventSink: (RoomListEvents) -> Unit,
onVerifyClicked: () -> Unit,
onConfirmRecoveryKeyClicked: () -> Unit,
onRoomClicked: (RoomListRoomSummary) -> Unit,
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
onInvitesClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val lazyListState = rememberLazyListState()
val visibleRange by remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
val size = layoutInfo.visibleItemsInfo.size
firstItemIndex until firstItemIndex + size
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
return super.onPostFling(consumed, available)
}
}
}
LazyColumn(
state = lazyListState,
modifier = modifier.nestedScroll(nestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
when (state.securityBannerState) {
SecurityBannerState.SessionVerification -> {
item {
RequestVerificationHeader(
onVerifyClicked = onVerifyClicked,
onDismissClicked = { eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
)
}
}
SecurityBannerState.RecoveryKeyConfirmation -> {
item {
ConfirmRecoveryKeyBanner(
onContinueClicked = onConfirmRecoveryKeyClicked,
onDismissClicked = { eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
)
}
}
else -> Unit
}
if (state.invitesState != InvitesState.NoInvites) {
item {
InvitesEntryPointView(onInvitesClicked, state.invitesState)
}
}
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
// is moved to the top of the list.
itemsIndexed(
items = state.summaries,
contentType = { _, room -> room.contentType() },
) { index, room ->
RoomSummaryRow(
room = room,
onClick = onRoomClicked,
onLongClick = onRoomLongClicked,
)
if (index != state.summaries.lastIndex) {
HorizontalDivider()
}
}
}
}
@Composable
private fun EmptyScaffold(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
subtitle: @Composable (() -> Unit)? = null,
action: @Composable (() -> Unit)? = null,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
title()
Spacer(modifier = Modifier.height(4.dp))
subtitle?.invoke()
Spacer(modifier = Modifier.height(16.dp))
action?.invoke()
}
}

View File

@@ -27,8 +27,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -62,19 +64,15 @@ class RoomListDataSource @Inject constructor(
roomListService
.allRooms
.summaries
.onStart {
// If we have no cached results, display a placeholder loading state
if (diffCache.isEmpty()) {
_allRooms.emit(RoomListRoomSummaryFactory.createFakeList())
}
}
.onEach { roomSummaries ->
replaceWith(roomSummaries)
}
.launchIn(coroutineScope)
}
val allRooms: SharedFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
val allRooms: Flow<ImmutableList<RoomListRoomSummary>> = _allRooms
val loadingState = roomListService.allRooms.loadingState
@OptIn(FlowPreview::class)
private fun observeNotificationSettings() {

View File

@@ -53,11 +53,6 @@ class RoomListRoomSummaryFactory @Inject constructor(
)
}
fun createFakeList(): ImmutableList<RoomListRoomSummary> {
return List(16) {
createPlaceholder("!fakeRoom$it:domain")
}.toImmutableList()
}
}
fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary {