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()) { LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) {
// If the last space is left, ensure that the Chat view is rendered. // If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered.
if (homeSpacesState.spaceRooms.isEmpty()) { if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) {
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
} }
} }

View file

@ -33,5 +33,5 @@ data class HomeState(
) { ) {
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters 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, lazyListState = spacesLazyListState,
onSpaceClick = { spaceId -> onSpaceClick = { spaceId ->
onRoomClick(spaceId) onRoomClick(spaceId)
} },
onCreateSpaceClick = onCreateSpaceClick,
// TODO use actual callbacks for this
onExploreClick = {},
) )
} }
} }

View file

@ -53,6 +53,8 @@ class HomeSpacesPresenter(
seenSpaceInvites = seenSpaceInvites, seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces, canCreateSpaces = canCreateSpaces,
// TODO enable once we can link to the screen to explore public spaces
canExploreSpaces = false,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

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

View file

@ -37,6 +37,11 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
spaceRooms = aListOfSpaceRooms(), spaceRooms = aListOfSpaceRooms(),
canCreateSpaces = false, canCreateSpaces = false,
), ),
aHomeSpacesState(
space = CurrentSpace.Root,
spaceRooms = emptyList(),
canCreateSpaces = true,
),
) )
} }
@ -46,6 +51,7 @@ internal fun aHomeSpacesState(
seenSpaceInvites: Set<RoomId> = emptySet(), seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false, hideInvitesAvatar: Boolean = false,
canCreateSpaces: Boolean = true, canCreateSpaces: Boolean = true,
canExploreSpaces: Boolean = true,
eventSink: (HomeSpacesEvents) -> Unit = {}, eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState( ) = HomeSpacesState(
space = space, space = space,
@ -53,6 +59,7 @@ internal fun aHomeSpacesState(
seenSpaceInvites = seenSpaceInvites.toImmutableSet(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces, canCreateSpaces = canCreateSpaces,
canExploreSpaces = canExploreSpaces,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -8,23 +8,40 @@
package io.element.android.features.home.impl.spaces 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.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview 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.Button
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider 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.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership 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.SpaceHeaderRootView
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView 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.components.SpaceRoomItemView
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
@ -32,56 +49,119 @@ fun HomeSpacesView(
state: HomeSpacesState, state: HomeSpacesState,
lazyListState: LazyListState, lazyListState: LazyListState,
onSpaceClick: (RoomId) -> Unit, onSpaceClick: (RoomId) -> Unit,
onCreateSpaceClick: () -> Unit,
onExploreClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( if (state.canCreateSpaces && state.spaceRooms.isEmpty()) {
modifier = modifier, EmptySpaceHomeView(
state = lazyListState modifier = modifier,
) { onCreateSpaceClick = onCreateSpaceClick,
val space = state.space onExploreClick = onExploreClick,
when (space) { canExploreSpaces = state.canExploreSpaces,
CurrentSpace.Root -> { )
item { } else {
SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size) 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( item {
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) {
HorizontalDivider() 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 @PreviewsDayNight
@Composable @Composable
internal fun HomeSpacesViewPreview( internal fun HomeSpacesViewPreview(
@ -91,6 +171,7 @@ internal fun HomeSpacesViewPreview(
state = state, state = state,
lazyListState = rememberLazyListState(), lazyListState = rememberLazyListState(),
onSpaceClick = {}, onSpaceClick = {},
modifier = Modifier, onCreateSpaceClick = {},
onExploreClick = {},
) )
} }

View file

@ -173,7 +173,7 @@ class HomePresenterTest {
} }
@Test @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 homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
val presenter = createHomePresenter( val presenter = createHomePresenter(
sessionStore = InMemorySessionStore( sessionStore = InMemorySessionStore(
@ -193,7 +193,7 @@ class HomePresenterTest {
val spaceState = awaitItem() val spaceState = awaitItem()
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
// The last space is left // The last space is left
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList())) homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false))
skipItems(1) skipItems(1)
val finalState = awaitItem() val finalState = awaitItem()
// We are back to Chats // We are back to Chats

View file

@ -93,6 +93,7 @@
<string name="action_enable">"Enable"</string> <string name="action_enable">"Enable"</string>
<string name="action_end_poll">"End poll"</string> <string name="action_end_poll">"End poll"</string>
<string name="action_enter_pin">"Enter PIN"</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_finish">"Finish"</string>
<string name="action_forgot_password">"Forgot password?"</string> <string name="action_forgot_password">"Forgot password?"</string>
<string name="action_forward">"Forward"</string> <string name="action_forward">"Forward"</string>

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:119b7216f0be644eb1153439eebdaf0ebb9acae3b68161c66fa0fa8c21db8703
size 24868

View file

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