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 <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-01-21 10:36:12 +01:00
committed by GitHub
parent 1cbf7d9624
commit 941340f250
11 changed files with 149 additions and 48 deletions

View File

@@ -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
}
}

View File

@@ -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()
}

View File

@@ -268,7 +268,10 @@ private fun HomeScaffold(
lazyListState = spacesLazyListState,
onSpaceClick = { spaceId ->
onRoomClick(spaceId)
}
},
onCreateSpaceClick = onCreateSpaceClick,
// TODO use actual callbacks for this
onExploreClick = {},
)
}
}

View File

@@ -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,
)
}

View File

@@ -19,6 +19,7 @@ data class HomeSpacesState(
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val canCreateSpaces: Boolean,
val canExploreSpaces: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
)

View File

@@ -37,6 +37,11 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
spaceRooms = aListOfSpaceRooms(),
canCreateSpaces = false,
),
aHomeSpacesState(
space = CurrentSpace.Root,
spaceRooms = emptyList(),
canCreateSpaces = true,
),
)
}
@@ -46,6 +51,7 @@ internal fun aHomeSpacesState(
seenSpaceInvites: Set<RoomId> = 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,
)

View File

@@ -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 = {},
)
}

View File

@@ -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

View File

@@ -93,6 +93,7 @@
<string name="action_enable">"Enable"</string>
<string name="action_end_poll">"End poll"</string>
<string name="action_enter_pin">"Enter PIN"</string>
<string name="action_explore_public_spaces">"Explore public spaces"</string>
<string name="action_finish">"Finish"</string>
<string name="action_forgot_password">"Forgot password?"</string>
<string name="action_forward">"Forward"</string>