From 941340f250b9e10cf4c8ed495083f0d83ab8bb7b Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 21 Jan 2026 10:36:12 +0100 Subject: [PATCH] Add empty state view for HomeSpacesView (#6047) * Add empty state view for `HomeSpacesView` This links to the create space flow, and has an 'explore public spaces', hidden for now. * Make sure we display the empty view if the 'create spaces' FF is enabled Also, remove the tab and navigate to the chats tab if the FF is disabled and the last space is left * Update screenshots --------- Co-authored-by: ElementBot --- .../features/home/impl/HomePresenter.kt | 6 +- .../android/features/home/impl/HomeState.kt | 2 +- .../android/features/home/impl/HomeView.kt | 5 +- .../home/impl/spaces/HomeSpacesPresenter.kt | 2 + .../home/impl/spaces/HomeSpacesState.kt | 1 + .../impl/spaces/HomeSpacesStateProvider.kt | 7 + .../home/impl/spaces/HomeSpacesView.kt | 163 +++++++++++++----- .../features/home/impl/HomePresenterTest.kt | 4 +- .../src/main/res/values/localazy.xml | 1 + ...me.impl.spaces_HomeSpacesView_Day_3_en.png | 3 + ....impl.spaces_HomeSpacesView_Night_3_en.png | 3 + 11 files changed, 149 insertions(+), 48 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png 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 3f223135c1..e03b40e840 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 @@ -94,9 +94,9 @@ class HomePresenter( } } - LaunchedEffect(homeSpacesState.spaceRooms.isEmpty()) { - // If the last space is left, ensure that the Chat view is rendered. - if (homeSpacesState.spaceRooms.isEmpty()) { + LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) { + // If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered. + if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) { currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal } } 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 474fb6d5ba..1850d2d4cc 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 @@ -33,5 +33,5 @@ data class HomeState( ) { val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters - val showNavigationBar = homeSpacesState.spaceRooms.isNotEmpty() + val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() } 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 f55183feb0..52745108f6 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 @@ -268,7 +268,10 @@ private fun HomeScaffold( lazyListState = spacesLazyListState, onSpaceClick = { spaceId -> onRoomClick(spaceId) - } + }, + onCreateSpaceClick = onCreateSpaceClick, + // TODO use actual callbacks for this + onExploreClick = {}, ) } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt index 00129235fe..d9e6aaa4d3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -53,6 +53,8 @@ class HomeSpacesPresenter( seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, canCreateSpaces = canCreateSpaces, + // TODO enable once we can link to the screen to explore public spaces + canExploreSpaces = false, eventSink = ::handleEvent, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt index 9bcf7131c8..84b2dc7f52 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt @@ -19,6 +19,7 @@ data class HomeSpacesState( val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, val canCreateSpaces: Boolean, + val canExploreSpaces: Boolean, val eventSink: (HomeSpacesEvents) -> Unit, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt index c1a32a1f34..a65f29cc2f 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt @@ -37,6 +37,11 @@ open class HomeSpacesStateProvider : PreviewParameterProvider { spaceRooms = aListOfSpaceRooms(), canCreateSpaces = false, ), + aHomeSpacesState( + space = CurrentSpace.Root, + spaceRooms = emptyList(), + canCreateSpaces = true, + ), ) } @@ -46,6 +51,7 @@ internal fun aHomeSpacesState( seenSpaceInvites: Set = emptySet(), hideInvitesAvatar: Boolean = false, canCreateSpaces: Boolean = true, + canExploreSpaces: Boolean = true, eventSink: (HomeSpacesEvents) -> Unit = {}, ) = HomeSpacesState( space = space, @@ -53,6 +59,7 @@ internal fun aHomeSpacesState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, canCreateSpaces = canCreateSpaces, + canExploreSpaces = canExploreSpaces, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt index 2505cf831d..7d7deab688 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -8,23 +8,40 @@ package io.element.android.features.home.impl.spaces +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment 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 io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +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.TextButton import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView import io.element.android.libraries.matrix.ui.components.SpaceHeaderView import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList @Composable @@ -32,56 +49,119 @@ fun HomeSpacesView( state: HomeSpacesState, lazyListState: LazyListState, onSpaceClick: (RoomId) -> Unit, + onCreateSpaceClick: () -> Unit, + onExploreClick: () -> Unit, modifier: Modifier = Modifier, ) { - LazyColumn( - modifier = modifier, - state = lazyListState - ) { - val space = state.space - when (space) { - CurrentSpace.Root -> { - item { - SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size) + if (state.canCreateSpaces && state.spaceRooms.isEmpty()) { + EmptySpaceHomeView( + modifier = modifier, + onCreateSpaceClick = onCreateSpaceClick, + onExploreClick = onExploreClick, + canExploreSpaces = state.canExploreSpaces, + ) + } else { + LazyColumn( + modifier = modifier, + state = lazyListState + ) { + val space = state.space + when (space) { + CurrentSpace.Root -> { + item { + SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size) + } + } + is CurrentSpace.Space -> { + item { + SpaceHeaderView( + avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader), + name = space.spaceRoom.displayName, + topic = space.spaceRoom.topic, + visibility = space.spaceRoom.visibility, + heroes = space.spaceRoom.heroes.toImmutableList(), + numberOfMembers = space.spaceRoom.numJoinedMembers, + ) + } } } - is CurrentSpace.Space -> item { - SpaceHeaderView( - avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader), - name = space.spaceRoom.displayName, - topic = space.spaceRoom.topic, - visibility = space.spaceRoom.visibility, - heroes = space.spaceRoom.heroes.toImmutableList(), - numberOfMembers = space.spaceRoom.numJoinedMembers, - ) - } - } - item { - HorizontalDivider() - } - itemsIndexed( - items = state.spaceRooms, - key = { _, spaceRoom -> spaceRoom.roomId } - ) { index, spaceRoom -> - val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED - SpaceRoomItemView( - spaceRoom = spaceRoom, - showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, - hideAvatars = isInvitation && state.hideInvitesAvatar, - onClick = { - onSpaceClick(spaceRoom.roomId) - }, - onLongClick = { - // TODO - }, - ) - if (index != state.spaceRooms.lastIndex) { + + item { HorizontalDivider() } + + itemsIndexed( + items = state.spaceRooms, + key = { _, spaceRoom -> spaceRoom.roomId } + ) { index, spaceRoom -> + val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED + SpaceRoomItemView( + spaceRoom = spaceRoom, + showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, + hideAvatars = isInvitation && state.hideInvitesAvatar, + onClick = { + onSpaceClick(spaceRoom.roomId) + }, + onLongClick = { + // TODO + }, + ) + if (index != state.spaceRooms.lastIndex) { + HorizontalDivider() + } + } } } } +@Composable +private fun EmptySpaceHomeView( + onCreateSpaceClick: () -> Unit, + onExploreClick: () -> Unit, + canExploreSpaces: Boolean, + modifier: Modifier = Modifier, +) { + HeaderFooterPage( + modifier = modifier, + topBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp, bottom = 16.dp, start = 40.dp, end = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + BigIcon( + style = BigIcon.Style.Default(CompoundIcons.SpaceSolid()) + ) + Text( + text = stringResource(CommonStrings.screen_space_list_empty_state_title), + style = ElementTheme.typography.fontHeadingLgBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + } + }, + footer = { + ButtonColumnMolecule { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_create_space), + onClick = onCreateSpaceClick, + ) + if (canExploreSpaces) { + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_explore_public_spaces), + onClick = onExploreClick, + ) + } + } + } + ) { + } +} + @PreviewsDayNight @Composable internal fun HomeSpacesViewPreview( @@ -91,6 +171,7 @@ internal fun HomeSpacesViewPreview( state = state, lazyListState = rememberLazyListState(), onSpaceClick = {}, - modifier = Modifier, + onCreateSpaceClick = {}, + onExploreClick = {}, ) } 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 266c33015d..37ed6e6909 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 @@ -173,7 +173,7 @@ class HomePresenterTest { } @Test - fun `present - NavigationBar is hidden when the last space is left`() = runTest { + fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest { val homeSpacesPresenter = MutablePresenter(aHomeSpacesState()) val presenter = createHomePresenter( sessionStore = InMemorySessionStore( @@ -193,7 +193,7 @@ class HomePresenterTest { val spaceState = awaitItem() assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) // The last space is left - homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList())) + homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false)) skipItems(1) val finalState = awaitItem() // We are back to Chats diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 7940aa3855..332151e5f8 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -93,6 +93,7 @@ "Enable" "End poll" "Enter PIN" + "Explore public spaces" "Finish" "Forgot password?" "Forward" diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png new file mode 100644 index 0000000000..c8cfca0ff6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:119b7216f0be644eb1153439eebdaf0ebb9acae3b68161c66fa0fa8c21db8703 +size 24868 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png new file mode 100644 index 0000000000..f0095cc4b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec42ba9eb13978cadb3d556482413eda959178d9a32743c2469ac3702541e8b5 +size 24247