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:
parent
1cbf7d9624
commit
941340f250
11 changed files with 149 additions and 48 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:119b7216f0be644eb1153439eebdaf0ebb9acae3b68161c66fa0fa8c21db8703
|
||||||
|
size 24868
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ec42ba9eb13978cadb3d556482413eda959178d9a32743c2469ac3702541e8b5
|
||||||
|
size 24247
|
||||||
Loading…
Add table
Add a link
Reference in a new issue