Add an empty state for the space screen if the user can modify its graph (#6064)
* Add an empty state for the space screen if the user can modify its graph. It adds a new 'create room' button that allows you to open the create room screen with some preset values. * When computing the editable spaces in `ConfigureRoomPresenter`, also set up the initial selected parent space if possible * Use `Builder` pattern for `CreateRoomEntryPoint` * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
cc4bf95cac
commit
4b4492681b
@@ -38,6 +38,7 @@ dependencies {
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.features.createroom.api)
|
||||
implementation(projects.features.invite.api)
|
||||
implementation(projects.libraries.previewutils)
|
||||
implementation(projects.features.securityandprivacy.api)
|
||||
@@ -49,5 +50,6 @@ dependencies {
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.features.createroom.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.addroom.AddRoomToSpaceNode
|
||||
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
||||
@@ -49,6 +50,7 @@ class SpaceFlowNode(
|
||||
room: JoinedRoom,
|
||||
spaceService: SpaceService,
|
||||
graphFactory: SpaceFlowGraph.Factory,
|
||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
@@ -71,6 +73,9 @@ class SpaceFlowNode(
|
||||
@Parcelize
|
||||
data object Leave : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object CreateRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AddRoom : NavTarget
|
||||
}
|
||||
@@ -116,6 +121,10 @@ class SpaceFlowNode(
|
||||
backstack.push(NavTarget.Leave)
|
||||
}
|
||||
|
||||
override fun onCreateRoom() {
|
||||
backstack.push(NavTarget.CreateRoom)
|
||||
}
|
||||
|
||||
override fun navigateToAddRoom() {
|
||||
backstack.push(NavTarget.AddRoom)
|
||||
}
|
||||
@@ -140,6 +149,21 @@ class SpaceFlowNode(
|
||||
}
|
||||
createNode<SpaceSettingsFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
is NavTarget.CreateRoom -> {
|
||||
val callback = object : CreateRoomEntryPoint.Callback {
|
||||
override fun onRoomCreated(roomId: RoomId) {
|
||||
callback.navigateToRoom(roomId, emptyList())
|
||||
}
|
||||
}
|
||||
createRoomEntryPoint
|
||||
.builder(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
.setParentSpace(spaceRoomList.roomId)
|
||||
.build()
|
||||
}
|
||||
NavTarget.AddRoom -> {
|
||||
val callback = object : AddRoomToSpaceNode.Callback {
|
||||
override fun onFinish() {
|
||||
|
||||
@@ -47,6 +47,8 @@ class SpaceNode(
|
||||
fun navigateToRoomMemberList()
|
||||
fun startLeaveSpaceFlow()
|
||||
fun navigateToAddRoom()
|
||||
|
||||
fun onCreateRoom()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
@@ -105,6 +107,7 @@ class SpaceNode(
|
||||
modifier = Modifier
|
||||
)
|
||||
},
|
||||
onCreateRoomClick = callback::onCreateRoom,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||
spaceInfo = aSpaceInfo(),
|
||||
children = aListOfSpaceRooms(),
|
||||
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
||||
hasMoreToLoad = false
|
||||
hasMoreToLoad = true,
|
||||
),
|
||||
aSpaceState(
|
||||
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
||||
@@ -71,7 +71,7 @@ fun aSpaceState(
|
||||
joiningRooms: Set<RoomId> = emptySet(),
|
||||
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
|
||||
hideInvitesAvatar: Boolean = false,
|
||||
hasMoreToLoad: Boolean = true,
|
||||
hasMoreToLoad: Boolean = false,
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
||||
canAccessSpaceSettings: Boolean = true,
|
||||
|
||||
@@ -50,7 +50,9 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.space.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
@@ -65,6 +67,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
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.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||
@@ -72,6 +75,7 @@ import io.element.android.libraries.designsystem.theme.components.DropdownMenuIt
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
@@ -98,6 +102,7 @@ fun SpaceView(
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onViewMembersClick: () -> Unit,
|
||||
onCreateRoomClick: () -> Unit,
|
||||
onAddRoomClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
acceptDeclineInviteView: @Composable () -> Unit,
|
||||
@@ -161,7 +166,8 @@ fun SpaceView(
|
||||
},
|
||||
onTopicClick = { topic ->
|
||||
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
||||
}
|
||||
},
|
||||
onCreateRoomClick = onCreateRoomClick,
|
||||
)
|
||||
JoinFailuresEffect(
|
||||
hasAnyFailure = state.hasAnyJoinFailures,
|
||||
@@ -234,6 +240,7 @@ private fun SpaceViewContent(
|
||||
state: SpaceState,
|
||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||
onTopicClick: (String) -> Unit,
|
||||
onCreateRoomClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier.fillMaxSize()) {
|
||||
@@ -259,61 +266,90 @@ private fun SpaceViewContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(
|
||||
items = state.children,
|
||||
key = { _, spaceRoom -> spaceRoom.roomId }
|
||||
) { index, spaceRoom ->
|
||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||
val isSelected = state.isSelected(spaceRoom.roomId)
|
||||
val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode
|
||||
SpaceRoomItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = showUnreadIndicator,
|
||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||
onClick = {
|
||||
onRoomClick(spaceRoom)
|
||||
},
|
||||
onLongClick = {
|
||||
// TODO
|
||||
},
|
||||
trailingAction = if (state.isManageMode) {
|
||||
{
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null,
|
||||
|
||||
if (state.children.isEmpty() && state.canEditSpaceGraph && !state.hasMoreToLoad) {
|
||||
item {
|
||||
EmptySpaceView(onCreateRoomClick = onCreateRoomClick)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items = state.children,
|
||||
key = { _, spaceRoom -> spaceRoom.roomId }
|
||||
) { index, spaceRoom ->
|
||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||
val isSelected = state.isSelected(spaceRoom.roomId)
|
||||
val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode
|
||||
SpaceRoomItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = showUnreadIndicator,
|
||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||
onClick = {
|
||||
onRoomClick(spaceRoom)
|
||||
},
|
||||
onLongClick = {
|
||||
// TODO
|
||||
},
|
||||
trailingAction = if (state.isManageMode) {
|
||||
{
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
||||
state.eventSink(SpaceEvents.Join(spaceRoom))
|
||||
}
|
||||
},
|
||||
bottomAction = if (state.isManageMode) {
|
||||
null
|
||||
} else {
|
||||
spaceRoom.inviteButtons(
|
||||
onAcceptClick = {
|
||||
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
||||
},
|
||||
onDeclineClick = {
|
||||
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
||||
state.eventSink(SpaceEvents.Join(spaceRoom))
|
||||
}
|
||||
},
|
||||
bottomAction = if (state.isManageMode) {
|
||||
null
|
||||
} else {
|
||||
spaceRoom.inviteButtons(
|
||||
onAcceptClick = {
|
||||
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
||||
},
|
||||
onDeclineClick = {
|
||||
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
||||
}
|
||||
)
|
||||
)
|
||||
if (index != state.children.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.hasMoreToLoad) {
|
||||
item {
|
||||
LoadingMoreIndicator(eventSink = state.eventSink)
|
||||
}
|
||||
)
|
||||
if (index != state.children.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
if (state.hasMoreToLoad) {
|
||||
item {
|
||||
LoadingMoreIndicator(eventSink = state.eventSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptySpaceView(onCreateRoomClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(bottom = 24.dp),
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
title = stringResource(R.string.screen_space_empty_state_title),
|
||||
subTitle = null,
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Room()),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 24.dp),
|
||||
)
|
||||
Button(
|
||||
text = stringResource(R.string.screen_space_add_room_action),
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Plus()),
|
||||
onClick = onCreateRoomClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingMoreIndicator(
|
||||
eventSink: (SpaceEvents) -> Unit,
|
||||
@@ -611,6 +647,7 @@ internal fun SpaceViewPreview(
|
||||
acceptDeclineInviteView = {},
|
||||
onSettingsClick = {},
|
||||
onViewMembersClick = {},
|
||||
onCreateRoomClick = {},
|
||||
onAddRoomClick = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.FakeCreateRoomEntryPoint
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -43,7 +44,8 @@ class DefaultSpaceEntryPointTest {
|
||||
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
||||
),
|
||||
room = FakeJoinedRoom(),
|
||||
graphFactory = FakeSpaceFlowGraph.Factory
|
||||
graphFactory = FakeSpaceFlowGraph.Factory,
|
||||
createRoomEntryPoint = FakeCreateRoomEntryPoint(),
|
||||
)
|
||||
}
|
||||
val callback = object : SpaceEntryPoint.Callback {
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.space.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
@@ -30,6 +31,7 @@ import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
@@ -200,6 +202,22 @@ class SpaceViewTest {
|
||||
rule.clickOn(CommonStrings.action_remove, inDialog = true)
|
||||
eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking create room button calls the expected callback`() {
|
||||
val onCreateRoomClick = lambdaRecorder<Unit> { }
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
children = emptyList(),
|
||||
hasMoreToLoad = false,
|
||||
isManageMode = true,
|
||||
canManageRooms = true,
|
||||
),
|
||||
onCreateRoomClick = onCreateRoomClick,
|
||||
)
|
||||
rule.clickOn(R.string.screen_space_add_room_action)
|
||||
onCreateRoomClick.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
||||
@@ -210,6 +228,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||
onAddRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||
) {
|
||||
@@ -224,6 +243,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||
onViewMembersClick = onViewMembersClick,
|
||||
onAddRoomClick = onAddRoomClick,
|
||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||
onCreateRoomClick = onCreateRoomClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user