diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 5ea238081b..88184bcbcd 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -51,6 +51,8 @@ dependencies { implementation(projects.features.rageshake.api) implementation(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) + implementation(libs.haze) + implementation(libs.haze.materials) implementation(projects.features.reportroom.api) api(projects.features.home.api) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt index 0762bbfb5a..4632e40d5a 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeEvents.kt @@ -7,4 +7,6 @@ package io.element.android.features.home.impl -sealed interface HomeEvents +sealed interface HomeEvents { + data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt new file mode 100644 index 0000000000..5254648f24 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt @@ -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 } + } + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index b1160b4b6b..894ed29e8d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -10,14 +10,20 @@ package io.element.android.features.home.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf 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.logout.api.direct.DirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher 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.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.sync.SyncService @@ -31,6 +37,7 @@ class HomePresenter @Inject constructor( private val roomListPresenter: Presenter, private val logoutPresenter: Presenter, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): HomeState { @@ -38,31 +45,42 @@ class HomePresenter @Inject constructor( val isOnline by syncService.isOnline.collectAsState() val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } 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) { // Force a refresh of the profile client.getUserProfile() } - // Avatar indicator val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() - val directLogoutState = logoutPresenter.present() fun handleEvents(event: HomeEvents) { - // TODO + when (event) { + is HomeEvents.SelectHomeNavigationBarItem -> { + currentHomeNavigationBarItemOrdinal = event.item.ordinal + } + } } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - return HomeState( matrixUser = matrixUser.value, showAvatarIndicator = showAvatarIndicator, hasNetworkConnection = isOnline, + currentHomeNavigationBarItem = currentHomeNavigationBarItem, roomListState = roomListState, snackbarMessage = snackbarMessage, canReportBug = canReportBug, directLogoutState = directLogoutState, + isSpaceFeatureEnabled = isSpaceFeatureEnabled, eventSink = ::handleEvents, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index f7b4f644a6..5e6c16d2e4 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -18,11 +18,13 @@ data class HomeState( val matrixUser: MatrixUser, val showAvatarIndicator: Boolean, val hasNetworkConnection: Boolean, + val currentHomeNavigationBarItem: HomeNavigationBarItem, val roomListState: RoomListState, val snackbarMessage: SnackbarMessage?, val canReportBug: Boolean, val directLogoutState: DirectLogoutState, + val isSpaceFeatureEnabled: Boolean, val eventSink: (HomeEvents) -> Unit, ) { - val displayActions = true + val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt index 621a36bf5e..7a50296e17 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt @@ -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.RoomListStateProvider 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.aDirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -24,6 +26,19 @@ open class HomeStateProvider : PreviewParameterProvider { aHomeState(), aHomeState(hasNetworkConnection = false), 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 { aHomeState(roomListState = it) } @@ -34,8 +49,10 @@ internal fun aHomeState( showAvatarIndicator: Boolean = false, hasNetworkConnection: Boolean = true, snackbarMessage: SnackbarMessage? = null, + currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats, roomListState: RoomListState = aRoomListState(), canReportBug: Boolean = true, + isSpaceFeatureEnabled: Boolean = false, directLogoutState: DirectLogoutState = aDirectLogoutState(), eventSink: (HomeEvents) -> Unit = {} ) = HomeState( @@ -45,6 +62,8 @@ internal fun aHomeState( snackbarMessage = snackbarMessage, canReportBug = canReportBug, directLogoutState = directLogoutState, + currentHomeNavigationBarItem = currentHomeNavigationBarItem, roomListState = roomListState, + isSpaceFeatureEnabled = isSpaceFeatureEnabled, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index fbd47bf52b..fb5b9c9ae3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -5,10 +5,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalHazeMaterialsApi::class) + package io.element.android.features.home.impl +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background 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.fillMaxSize import androidx.compose.foundation.layout.padding @@ -19,10 +24,19 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource 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.tokens.generated.CompoundIcons 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.theme.components.FloatingActionButton 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.Text import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.RoomId @@ -138,10 +155,19 @@ private fun HomeScaffold( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val roomListState: RoomListState = state.roomListState + BackHandler( + enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats, + ) { + state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + } + + val hazeState = rememberHazeState() + Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { RoomListTopBar( + title = stringResource(state.currentHomeNavigationBarItem.labelRes), matrixUser = state.matrixUser, showAvatarIndicator = state.showAvatarIndicator, areSearchResultsDisplayed = roomListState.searchState.isSearchActive, @@ -150,25 +176,83 @@ private fun HomeScaffold( onOpenSettings = onOpenSettings, scrollBehavior = scrollBehavior, displayMenuItems = state.displayActions, - displayFilters = roomListState.displayFilters, + displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats, filtersState = roomListState.filtersState, 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 -> - RoomListContentView( - contentState = roomListState.contentState, - filtersState = roomListState.filtersState, - hideInvitesAvatars = roomListState.hideInvitesAvatars, - eventSink = roomListState.eventSink, - onSetUpRecoveryClick = onSetUpRecoveryClick, - onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, - onRoomClick = ::onRoomClick, - onCreateRoomClick = onCreateRoomClick, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - ) + when (state.currentHomeNavigationBarItem) { + HomeNavigationBarItem.Chats -> { + RoomListContentView( + contentState = roomListState.contentState, + filtersState = roomListState.filtersState, + hideInvitesAvatars = roomListState.hideInvitesAvatars, + eventSink = roomListState.eventSink, + onSetUpRecoveryClick = onSetUpRecoveryClick, + onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, + onRoomClick = ::onRoomClick, + onCreateRoomClick = onCreateRoomClick, + // 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 = { if (state.displayActions) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index c4144b22e9..993fb0b85c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -66,6 +67,7 @@ fun RoomListContentView( onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, onCreateRoomClick: () -> Unit, + contentBottomPadding: Dp, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { @@ -93,6 +95,7 @@ fun RoomListContentView( onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onRoomClick = onRoomClick, + contentBottomPadding = contentBottomPadding, ) } } @@ -164,6 +167,7 @@ private fun RoomsView( onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, + contentBottomPadding: Dp, modifier: Modifier = Modifier, ) { if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { @@ -179,6 +183,7 @@ private fun RoomsView( onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onRoomClick = onRoomClick, + contentBottomPadding = contentBottomPadding, modifier = modifier.fillMaxSize(), ) } @@ -192,6 +197,7 @@ private fun RoomsViewList( onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, + contentBottomPadding: Dp, modifier: Modifier = Modifier, ) { val lazyListState = rememberLazyListState() @@ -210,8 +216,7 @@ private fun RoomsViewList( LazyColumn( state = lazyListState, modifier = modifier, - // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 - contentPadding = PaddingValues(bottom = 80.dp) + contentPadding = PaddingValues(bottom = contentBottomPadding) ) { when (state.securityBannerState) { SecurityBannerState.SetUpRecovery -> { @@ -324,5 +329,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr onConfirmRecoveryKeyClick = {}, onRoomClick = {}, onCreateRoomClick = {}, + contentBottomPadding = 0.dp, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt index 1b7edcbfd4..ae1d708933 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt @@ -76,6 +76,7 @@ private val avatarBloomSize = 430.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListTopBar( + title: String, matrixUser: MatrixUser, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, @@ -90,6 +91,7 @@ fun RoomListTopBar( modifier: Modifier = Modifier, ) { DefaultRoomListTopBar( + title = title, matrixUser = matrixUser, showAvatarIndicator = showAvatarIndicator, areSearchResultsDisplayed = areSearchResultsDisplayed, @@ -108,6 +110,7 @@ fun RoomListTopBar( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBar( + title: String, matrixUser: MatrixUser, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, @@ -194,7 +197,7 @@ private fun DefaultRoomListTopBar( scrolledContainerColor = Color.Transparent, ), title = { - Text(text = stringResource(id = R.string.screen_roomlist_main_space_title)) + Text(text = title) }, navigationIcon = { NavigationIcon( @@ -315,6 +318,7 @@ private fun NavigationIcon( @Composable internal fun DefaultRoomListTopBarPreview() = ElementPreview { DefaultRoomListTopBar( + title = stringResource(R.string.screen_roomlist_main_space_title), matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -334,6 +338,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview { @Composable internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { DefaultRoomListTopBar( + title = stringResource(R.string.screen_roomlist_main_space_title), matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), showAvatarIndicator = true, areSearchResultsDisplayed = false, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt index 9fd5324d57..0e4600c16a 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.push.api.battery.aBatteryOptimizationState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList open class RoomListStateProvider : PreviewParameterProvider { override val values: Sequence @@ -113,3 +114,18 @@ internal fun aRoomListRoomSummaryList(): ImmutableList { ), ) } + +internal fun generateRoomListRoomSummaryList( + numberOfRooms: Int = 10, +): ImmutableList { + 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() +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 0ca344cafd..6913ea19bf 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -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.rageshake.api.RageshakeFeatureAvailability 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.test.FakeIndicatorService 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.sync.FakeSyncService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -58,6 +62,21 @@ class HomePresenterTest { assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) 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( client: MatrixClient = FakeMatrixClient(), syncService: SyncService = FakeSyncService(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, indicatorService: IndicatorService = FakeIndicatorService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService() ) = HomePresenter( client = client, syncService = syncService, @@ -109,5 +143,6 @@ class HomePresenterTest { logoutPresenter = { aDirectLogoutState() }, roomListPresenter = { aRoomListState() }, rageshakeFeatureAvailability = rageshakeFeatureAvailability, + featureFlagService = featureFlagService, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b0a9ca619..f128a5b393 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ appyx = "1.7.1" sqldelight = "2.1.0" wysiwyg = "2.38.4" telephoto = "0.16.0" +haze = "1.6.4" # Dependency analysis 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" opusencoder = "io.element.android:opusencoder:1.2.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 posthog = "com.posthog:posthog-android:3.19.0" diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 00ab0fb57c..81c33318c1 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -130,6 +130,13 @@ enum class FeatureFlags( defaultValue = { 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( key = "feature.media_upload_through_send_queue", title = "Media upload through send queue", diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png index 1faef9acc6..2bc640fe3a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c -size 5788 +oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e +size 46034 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png index ca0f363c49..6e0088ee14 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3 -size 105294 +oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e +size 40681 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_12_en.png index b7e1278591..1faef9acc6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c -size 99928 +oid sha256:cd29643f2af1dd5edd37c6ebfc6059db3444810a9dc8002bb5abc8475b4b6b0c +size 5788 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png new file mode 100644 index 0000000000..ca0f363c49 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20e6d5a1c7218304ede990c1906ec628593acbe57a6496b98a2c1fe9c87d1ae3 +size 105294 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png new file mode 100644 index 0000000000..b7e1278591 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c +size 99928 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png index b3dfe3db1d..f6988f9732 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b -size 82238 +oid sha256:51d89e7c0e8c205c230351c4a00e0d560f3f54c6b423ae3b2b801fd6d00343bf +size 83509 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png index 20806bc42c..ec5767287f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367 -size 60691 +oid sha256:e7383bea26cbd09cff981e6b4202f7e9bca788094ea482241b45eaefcbf16127 +size 34927 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index 1b34b511fb..b3dfe3db1d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41 -size 60495 +oid sha256:a7b0dc90002ec59917ed0c64dae785088117c5aca61961c3d71dc7c05d2fa72b +size 82238 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png index 13db73a34e..20806bc42c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2 -size 58734 +oid sha256:4385b380efbae924d4a8fece278a61003d40acbd795fef1aa2eab4c5fadbf367 +size 60691 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png index 114ba675ce..1b34b511fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7 -size 99704 +oid sha256:7d64fa49cfca399eb073a8efac881aebbfed38726116d3113f9a562245ac9a41 +size 60495 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png index 2bc640fe3a..13db73a34e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:660e96d8c627d8f24bbfbc150a3e2b7e74f6819e7fbaa581c5872f544ed9685e -size 46034 +oid sha256:020d732b7b3a4f43918bee7b9424ffd03fe20074c5823341811a1c700551b2f2 +size 58734 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png index 6e0088ee14..114ba675ce 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e -size 40681 +oid sha256:63f8198b96e49f9a29dad22ef2249503d1528aec874599d9daab3f8634d662d7 +size 99704 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png index e9b928c558..81c7db17ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980 -size 5662 +oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82 +size 53250 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png index 19375d3ec5..66309e8b71 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c -size 111059 +oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c +size 47265 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_12_en.png index 6cafa75e88..e9b928c558 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d -size 105496 +oid sha256:d1931e55ba639d0732a75612f691b034fb1b5f2d82fa1e9719dadc6ae2c5f980 +size 5662 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png new file mode 100644 index 0000000000..19375d3ec5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c93289bc30c3b590360f916a55a86a5cc98028bd2201cb410f329de85ed615c +size 111059 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png new file mode 100644 index 0000000000..6cafa75e88 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d +size 105496 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png index 92d58a36cd..1c65bb2ce7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae -size 88613 +oid sha256:3962f724784add8dc33c7327dd866cab76eff0d53abb781b94e974ae68212e0b +size 90485 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png index 611da1630c..9b2a3650ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3 -size 69463 +oid sha256:4850c3002637166016ac75129c0fd4081c540848969b40eb84c4f60a4366c111 +size 43857 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index 642eca994d..92d58a36cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426 -size 69240 +oid sha256:ca1e0f43d447faf770ee7aa4f443f3a310f520c9437cbd5baca9ac7ce36d2aae +size 88613 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png index 0604b0bc73..611da1630c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348 -size 67496 +oid sha256:acde7691ec0ebe690bcb29478963107e2dc0fe6fa54892b3600f50e0cf1db2a3 +size 69463 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png index 48835abda6..642eca994d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4 -size 105496 +oid sha256:38e55635243d77c084053c4feea03dcedabd549aa556f4a622ec2d629bb4f426 +size 69240 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png index 81c7db17ae..0604b0bc73 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6be30df0d8f5ba0a57c720bcc16eefc3a5b7dacfc8115a8d47a42edab3e63e82 -size 53250 +oid sha256:1fc45bd4279456b75cadcb8381451f9e6a0e1fadaf4a8f22274c94aa89c7c348 +size 67496 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png index 66309e8b71..48835abda6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c -size 47265 +oid sha256:8d9f42109a12e450a13d1166f7d7790913e1dd847cd44f00a2c881d825cad9b4 +size 105496