Merge pull request #4964 from element-hq/feature/bma/roomListNavigationBar

Home navigation bar
This commit is contained in:
Benoit Marty 2025-07-01 17:22:21 +02:00 committed by GitHub
commit 8bc7bed5cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 313 additions and 64 deletions

View file

@ -51,6 +51,8 @@ dependencies {
implementation(projects.features.rageshake.api) implementation(projects.features.rageshake.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(projects.features.reportroom.api) implementation(projects.features.reportroom.api)
api(projects.features.home.api) api(projects.features.home.api)

View file

@ -7,4 +7,6 @@
package io.element.android.features.home.impl package io.element.android.features.home.impl
sealed interface HomeEvents sealed interface HomeEvents {
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import io.element.android.compound.tokens.generated.CompoundIcons
enum class HomeNavigationBarItem(
@StringRes
val labelRes: Int,
) {
Chats(
labelRes = R.string.screen_roomlist_main_space_title
),
Spaces(
// TODO Create a new entry in Localazy
labelRes = R.string.screen_roomlist_main_space_title
);
@Composable
fun icon() = when (this) {
Chats -> CompoundIcons.ChatSolid()
// TODO Spaces -> CompoundIcons.Workspace()
Spaces -> CompoundIcons.Code()
}
companion object {
fun from(index: Int): HomeNavigationBarItem {
return entries.getOrElse(index) { Chats }
}
}
}

View file

@ -10,14 +10,20 @@ package io.element.android.features.home.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
@ -31,6 +37,7 @@ class HomePresenter @Inject constructor(
private val roomListPresenter: Presenter<RoomListState>, private val roomListPresenter: Presenter<RoomListState>,
private val logoutPresenter: Presenter<DirectLogoutState>, private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
) : Presenter<HomeState> { ) : Presenter<HomeState> {
@Composable @Composable
override fun present(): HomeState { override fun present(): HomeState {
@ -38,31 +45,42 @@ class HomePresenter @Inject constructor(
val isOnline by syncService.isOnline.collectAsState() val isOnline by syncService.isOnline.collectAsState()
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
val roomListState = roomListPresenter.present() val roomListState = roomListPresenter.present()
val isSpaceFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
}.collectAsState(initial = false)
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
val currentHomeNavigationBarItem by remember {
derivedStateOf {
HomeNavigationBarItem.from(currentHomeNavigationBarItemOrdinal)
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Force a refresh of the profile // Force a refresh of the profile
client.getUserProfile() client.getUserProfile()
} }
// Avatar indicator // Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val directLogoutState = logoutPresenter.present() val directLogoutState = logoutPresenter.present()
fun handleEvents(event: HomeEvents) { fun handleEvents(event: HomeEvents) {
// TODO when (event) {
is HomeEvents.SelectHomeNavigationBarItem -> {
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
}
} }
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState( return HomeState(
matrixUser = matrixUser.value, matrixUser = matrixUser.value,
showAvatarIndicator = showAvatarIndicator, showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = isOnline, hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState, roomListState = roomListState,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
canReportBug = canReportBug, canReportBug = canReportBug,
directLogoutState = directLogoutState, directLogoutState = directLogoutState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }

View file

@ -18,11 +18,13 @@ data class HomeState(
val matrixUser: MatrixUser, val matrixUser: MatrixUser,
val showAvatarIndicator: Boolean, val showAvatarIndicator: Boolean,
val hasNetworkConnection: Boolean, val hasNetworkConnection: Boolean,
val currentHomeNavigationBarItem: HomeNavigationBarItem,
val roomListState: RoomListState, val roomListState: RoomListState,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean, val canReportBug: Boolean,
val directLogoutState: DirectLogoutState, val directLogoutState: DirectLogoutState,
val isSpaceFeatureEnabled: Boolean,
val eventSink: (HomeEvents) -> Unit, val eventSink: (HomeEvents) -> Unit,
) { ) {
val displayActions = true val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
} }

View file

@ -11,6 +11,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.roomlist.RoomListStateProvider import io.element.android.features.home.impl.roomlist.RoomListStateProvider
import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@ -24,6 +26,19 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
aHomeState(), aHomeState(),
aHomeState(hasNetworkConnection = false), aHomeState(hasNetworkConnection = false),
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aHomeState(
isSpaceFeatureEnabled = true,
roomListState = aRoomListState(
// Add more rooms to see the blur effect under the NavigationBar
contentState = aRoomsContentState(
summaries = generateRoomListRoomSummaryList(),
)
),
),
aHomeState(
isSpaceFeatureEnabled = true,
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
),
) + RoomListStateProvider().values.map { ) + RoomListStateProvider().values.map {
aHomeState(roomListState = it) aHomeState(roomListState = it)
} }
@ -34,8 +49,10 @@ internal fun aHomeState(
showAvatarIndicator: Boolean = false, showAvatarIndicator: Boolean = false,
hasNetworkConnection: Boolean = true, hasNetworkConnection: Boolean = true,
snackbarMessage: SnackbarMessage? = null, snackbarMessage: SnackbarMessage? = null,
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
roomListState: RoomListState = aRoomListState(), roomListState: RoomListState = aRoomListState(),
canReportBug: Boolean = true, canReportBug: Boolean = true,
isSpaceFeatureEnabled: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(), directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvents) -> Unit = {} eventSink: (HomeEvents) -> Unit = {}
) = HomeState( ) = HomeState(
@ -45,6 +62,8 @@ internal fun aHomeState(
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
canReportBug = canReportBug, canReportBug = canReportBug,
directLogoutState = directLogoutState, directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState, roomListState = roomListState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -5,10 +5,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalHazeMaterialsApi::class)
package io.element.android.features.home.impl package io.element.android.features.home.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -19,10 +24,19 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.chrisbanes.haze.rememberHazeState
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.components.RoomListContentView import io.element.android.features.home.impl.components.RoomListContentView
@ -41,7 +55,10 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.NavigationBar
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -138,10 +155,19 @@ private fun HomeScaffold(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val roomListState: RoomListState = state.roomListState val roomListState: RoomListState = state.roomListState
BackHandler(
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats,
) {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
}
val hazeState = rememberHazeState()
Scaffold( Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
RoomListTopBar( RoomListTopBar(
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
matrixUser = state.matrixUser, matrixUser = state.matrixUser,
showAvatarIndicator = state.showAvatarIndicator, showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = roomListState.searchState.isSearchActive, areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
@ -150,25 +176,83 @@ private fun HomeScaffold(
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
displayMenuItems = state.displayActions, displayMenuItems = state.displayActions,
displayFilters = roomListState.displayFilters, displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
filtersState = roomListState.filtersState, filtersState = roomListState.filtersState,
canReportBug = state.canReportBug, canReportBug = state.canReportBug,
) )
}, },
bottomBar = {
if (state.isSpaceFeatureEnabled) {
NavigationBar(
containerColor = Color.Transparent,
modifier = Modifier
.hazeEffect(
state = hazeState,
style = HazeMaterials.regular(),
)
) {
HomeNavigationBarItem.entries.forEach { item ->
NavigationBarItem(
selected = state.currentHomeNavigationBarItem == item,
onClick = {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
},
icon = {
Icon(
imageVector = item.icon(),
contentDescription = null
)
},
label = {
Text(stringResource(item.labelRes))
}
)
}
}
}
},
content = { padding -> content = { padding ->
RoomListContentView( when (state.currentHomeNavigationBarItem) {
contentState = roomListState.contentState, HomeNavigationBarItem.Chats -> {
filtersState = roomListState.filtersState, RoomListContentView(
hideInvitesAvatars = roomListState.hideInvitesAvatars, contentState = roomListState.contentState,
eventSink = roomListState.eventSink, filtersState = roomListState.filtersState,
onSetUpRecoveryClick = onSetUpRecoveryClick, hideInvitesAvatars = roomListState.hideInvitesAvatars,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, eventSink = roomListState.eventSink,
onRoomClick = ::onRoomClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onCreateRoomClick = onCreateRoomClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
modifier = Modifier onRoomClick = ::onRoomClick,
.padding(padding) onCreateRoomClick = onCreateRoomClick,
.consumeWindowInsets(padding) // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
) // and include provided bottom padding
contentBottomPadding = 80.dp + padding.calculateBottomPadding(),
modifier = Modifier
.padding(
top = padding.calculateTopPadding(),
bottom = 0.dp,
start = padding.calculateStartPadding(LocalLayoutDirection.current),
end = padding.calculateEndPadding(LocalLayoutDirection.current),
)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState)
)
}
HomeNavigationBarItem.Spaces -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Spaces are coming soon!",
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary,
)
}
}
}
}, },
floatingActionButton = { floatingActionButton = {
if (state.displayActions) { if (state.displayActions) {

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
@ -66,6 +67,7 @@ fun RoomListContentView(
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit, onCreateRoomClick: () -> Unit,
contentBottomPadding: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
@ -93,6 +95,7 @@ fun RoomListContentView(
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
contentBottomPadding = contentBottomPadding,
) )
} }
} }
@ -164,6 +167,7 @@ private fun RoomsView(
onSetUpRecoveryClick: () -> Unit, onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
contentBottomPadding: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@ -179,6 +183,7 @@ private fun RoomsView(
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
contentBottomPadding = contentBottomPadding,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} }
@ -192,6 +197,7 @@ private fun RoomsViewList(
onSetUpRecoveryClick: () -> Unit, onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
contentBottomPadding: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -210,8 +216,7 @@ private fun RoomsViewList(
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,
modifier = modifier, modifier = modifier,
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 contentPadding = PaddingValues(bottom = contentBottomPadding)
contentPadding = PaddingValues(bottom = 80.dp)
) { ) {
when (state.securityBannerState) { when (state.securityBannerState) {
SecurityBannerState.SetUpRecovery -> { SecurityBannerState.SetUpRecovery -> {
@ -324,5 +329,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
onConfirmRecoveryKeyClick = {}, onConfirmRecoveryKeyClick = {},
onRoomClick = {}, onRoomClick = {},
onCreateRoomClick = {}, onCreateRoomClick = {},
contentBottomPadding = 0.dp,
) )
} }

View file

@ -76,6 +76,7 @@ private val avatarBloomSize = 430.dp
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RoomListTopBar( fun RoomListTopBar(
title: String,
matrixUser: MatrixUser, matrixUser: MatrixUser,
showAvatarIndicator: Boolean, showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean, areSearchResultsDisplayed: Boolean,
@ -90,6 +91,7 @@ fun RoomListTopBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
DefaultRoomListTopBar( DefaultRoomListTopBar(
title = title,
matrixUser = matrixUser, matrixUser = matrixUser,
showAvatarIndicator = showAvatarIndicator, showAvatarIndicator = showAvatarIndicator,
areSearchResultsDisplayed = areSearchResultsDisplayed, areSearchResultsDisplayed = areSearchResultsDisplayed,
@ -108,6 +110,7 @@ fun RoomListTopBar(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun DefaultRoomListTopBar( private fun DefaultRoomListTopBar(
title: String,
matrixUser: MatrixUser, matrixUser: MatrixUser,
showAvatarIndicator: Boolean, showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean, areSearchResultsDisplayed: Boolean,
@ -194,7 +197,7 @@ private fun DefaultRoomListTopBar(
scrolledContainerColor = Color.Transparent, scrolledContainerColor = Color.Transparent,
), ),
title = { title = {
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title)) Text(text = title)
}, },
navigationIcon = { navigationIcon = {
NavigationIcon( NavigationIcon(
@ -315,6 +318,7 @@ private fun NavigationIcon(
@Composable @Composable
internal fun DefaultRoomListTopBarPreview() = ElementPreview { internal fun DefaultRoomListTopBarPreview() = ElementPreview {
DefaultRoomListTopBar( DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
showAvatarIndicator = false, showAvatarIndicator = false,
areSearchResultsDisplayed = false, areSearchResultsDisplayed = false,
@ -334,6 +338,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
@Composable @Composable
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
DefaultRoomListTopBar( DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
showAvatarIndicator = true, showAvatarIndicator = true,
areSearchResultsDisplayed = false, areSearchResultsDisplayed = false,

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> { open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState> override val values: Sequence<RoomListState>
@ -113,3 +114,18 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
), ),
) )
} }
internal fun generateRoomListRoomSummaryList(
numberOfRooms: Int = 10,
): ImmutableList<RoomListRoomSummary> {
return List(numberOfRooms) { index ->
aRoomListRoomSummary(
name = "Room#$index",
numberOfUnreadMessages = 0,
timestamp = "14:16",
lastMessage = "A message",
avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem),
id = "!roomId$index:domain",
)
}.toPersistentList()
}

View file

@ -15,6 +15,9 @@ import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.indicator.test.FakeIndicatorService import io.element.android.libraries.indicator.test.FakeIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -27,6 +30,7 @@ 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.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -58,6 +62,21 @@ class HomePresenterTest {
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(withUserState.showAvatarIndicator).isFalse() assertThat(withUserState.showAvatarIndicator).isFalse()
assertThat(withUserState.isSpaceFeatureEnabled).isFalse()
}
}
@Test
fun `present - space feature enabled`() = runTest {
val presenter = createHomePresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.Space.key to true),
),
)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isSpaceFeatureEnabled).isTrue()
} }
} }
@ -95,12 +114,27 @@ class HomePresenterTest {
} }
} }
@Test
fun `present - NavigationBar change`() = runTest {
val presenter = createHomePresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val finalState = awaitItem()
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
}
}
private fun TestScope.createHomePresenter( private fun TestScope.createHomePresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
syncService: SyncService = FakeSyncService(), syncService: SyncService = FakeSyncService(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
indicatorService: IndicatorService = FakeIndicatorService(), indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService()
) = HomePresenter( ) = HomePresenter(
client = client, client = client,
syncService = syncService, syncService = syncService,
@ -109,5 +143,6 @@ class HomePresenterTest {
logoutPresenter = { aDirectLogoutState() }, logoutPresenter = { aDirectLogoutState() },
roomListPresenter = { aRoomListState() }, roomListPresenter = { aRoomListState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability, rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
) )
} }

View file

@ -44,6 +44,7 @@ appyx = "1.7.1"
sqldelight = "2.1.0" sqldelight = "2.1.0"
wysiwyg = "2.38.4" wysiwyg = "2.38.4"
telephoto = "0.16.0" telephoto = "0.16.0"
haze = "1.6.4"
# Dependency analysis # Dependency analysis
dependencyAnalysis = "2.19.0" dependencyAnalysis = "2.19.0"
@ -191,6 +192,8 @@ maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0" opusencoder = "io.element.android:opusencoder:1.2.0"
zxing_cpp = "io.github.zxing-cpp:android:2.3.0" zxing_cpp = "io.github.zxing-cpp:android:2.3.0"
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.19.0" posthog = "com.posthog:posthog-android:3.19.0"

View file

@ -130,6 +130,13 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
Space(
key = "feature.space",
title = "Spaces",
description = "Spaces are under active development, only developers should enable this flog for now.",
defaultValue = { false },
isFinished = false,
),
MediaUploadOnSendQueue( MediaUploadOnSendQueue(
key = "feature.media_upload_through_send_queue", key = "feature.media_upload_through_send_queue",
title = "Media upload through send queue", title = "Media upload through send queue",

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e
size 5788 size 46034

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3 oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e
size 105294 size 40681

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c
size 99928 size 5788

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3
size 105294

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c
size 99928

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b oid sha256:51d89e7c0e8c205c230351c4a00e0d560f3f54c6b423ae3b2b801fd6d00343bf
size 82238 size 83509

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367 oid sha256:e7383bea26cbd09cff981e6b4202f7e9bca788094ea482241b45eaefcbf16127
size 60691 size 34927

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41 oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b
size 60495 size 82238

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2 oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367
size 58734 size 60691

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7 oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41
size 99704 size 60495

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2
size 46034 size 58734

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7
size 40681 size 99704

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980 oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82
size 5662 size 53250

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c
size 111059 size 47265

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980
size 105496 size 5662

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c
size 111059

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d
size 105496

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae oid sha256:3962f724784add8dc33c7327dd866cab76eff0d53abb781b94e974ae68212e0b
size 88613 size 90485

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3 oid sha256:4850c3002637166016ac75129c0fd4081c540848969b40eb84c4f60a4366c111
size 69463 size 43857

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426 oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae
size 69240 size 88613

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348 oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3
size 67496 size 69463

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4 oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426
size 105496 size 69240

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82 oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348
size 53250 size 67496

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4
size 47265 size 105496