diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 21bdd99a47..c76150a67e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -484,7 +484,10 @@ class LoggedInFlowNode( backstack.replace(NavTarget.Room(roomIdOrAlias = RoomIdOrAlias.Id(roomId), serverNames = emptyList())) } } - createRoomEntryPoint.createNode(isSpace = true, parentNode = this, buildContext = buildContext, callback = callback) + createRoomEntryPoint + .builder(parentNode = this, buildContext = buildContext, callback = callback) + .setIsSpace(true) + .build() } is NavTarget.SecureBackup -> { secureBackupEntryPoint.createNode( diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt index 22757aba06..271fdb2332 100644 --- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt @@ -15,12 +15,13 @@ import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.matrix.api.core.RoomId interface CreateRoomEntryPoint : FeatureEntryPoint { - fun createNode( - isSpace: Boolean, - parentNode: Node, - buildContext: BuildContext, - callback: Callback, - ): Node + interface Builder { + fun setIsSpace(isSpace: Boolean): Builder + fun setParentSpace(parentSpaceId: RoomId): Builder + fun build(): Node + } + + fun builder(parentNode: Node, buildContext: BuildContext, callback: Callback): Builder interface Callback : Plugin { fun onRoomCreated(roomId: RoomId) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 89dbddd186..d2cd1915f4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -38,7 +38,7 @@ class CreateRoomFlowNode( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.ConfigureRoom(isSpace = plugins.filterIsInstance().first().isSpace), + initialElement = initialElementFromInputs(plugins.filterIsInstance().first()), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -46,7 +46,8 @@ class CreateRoomFlowNode( ) { @Parcelize data class Inputs( - val isSpace: Boolean + val isSpace: Boolean, + val parentSpaceId: RoomId?, ) : NodeInputs, Parcelable private val callback: CreateRoomEntryPoint.Callback = callback() @@ -54,7 +55,7 @@ class CreateRoomFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.ConfigureRoom -> { - val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace) + val inputs = ConfigureRoomNode.Inputs(isSpace = navTarget.isSpace, parentSpaceId = navTarget.parentSpaceId) val callback = object : ConfigureRoomNode.Callback { override fun onCreateRoomSuccess(roomId: RoomId) { backstack.replace(NavTarget.AddPeople(roomId)) @@ -81,9 +82,14 @@ class CreateRoomFlowNode( sealed interface NavTarget : Parcelable { @Parcelize - data class ConfigureRoom(val isSpace: Boolean) : NavTarget + data class ConfigureRoom(val isSpace: Boolean, val parentSpaceId: RoomId?) : NavTarget @Parcelize data class AddPeople(val roomId: RoomId) : NavTarget } } + +private fun initialElementFromInputs(inputs: CreateRoomFlowNode.Inputs) = CreateRoomFlowNode.NavTarget.ConfigureRoom( + isSpace = inputs.isSpace, + parentSpaceId = inputs.parentSpaceId, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt index 63163e7a28..798467e48c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt @@ -14,16 +14,35 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId @ContributesBinding(SessionScope::class) class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint { - override fun createNode( - isSpace: Boolean, - parentNode: Node, - buildContext: BuildContext, - callback: CreateRoomEntryPoint.Callback, - ): Node { - val inputs = CreateRoomFlowNode.Inputs(isSpace) - return parentNode.createNode(buildContext, listOf(inputs, callback)) + class Builder( + private val parentNode: Node, + private val buildContext: BuildContext, + private val callback: CreateRoomEntryPoint.Callback, + ) : CreateRoomEntryPoint.Builder { + private var isSpace = false + private var parentSpaceId: RoomId? = null + + override fun setIsSpace(isSpace: Boolean): Builder { + this.isSpace = isSpace + return this + } + + override fun setParentSpace(parentSpaceId: RoomId): Builder { + this.parentSpaceId = parentSpaceId + return this + } + + override fun build(): Node { + val inputs = CreateRoomFlowNode.Inputs(isSpace = isSpace, parentSpaceId = parentSpaceId) + return parentNode.createNode(buildContext, listOf(inputs, callback)) + } + } + + override fun builder(parentNode: Node, buildContext: BuildContext, callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.Builder { + return Builder(parentNode, buildContext, callback) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt index e021a7a0f9..8652db76f8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -42,11 +42,12 @@ class ConfigureRoomNode( @Parcelize data class Inputs( val isSpace: Boolean, + val parentSpaceId: RoomId?, ) : NodeInputs, Parcelable private val inputs = inputs() - private val presenter = presenterFactory.create(inputs.isSpace) + private val presenter = presenterFactory.create(inputs.isSpace, inputs.parentSpaceId) init { lifecycle.subscribe( diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 8f231a440f..85c64adc83 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -63,6 +63,7 @@ import kotlin.time.Duration.Companion.seconds @AssistedInject class ConfigureRoomPresenter( @Assisted private val isSpace: Boolean, + @Assisted private val initialParentSpaceId: RoomId?, private val dataStore: CreateRoomConfigStore, private val matrixClient: MatrixClient, private val mediaPickerProvider: PickerProvider, @@ -75,7 +76,7 @@ class ConfigureRoomPresenter( ) : Presenter { @AssistedFactory interface Factory { - fun create(isSpace: Boolean): ConfigureRoomPresenter + fun create(isSpace: Boolean, parentSpaceId: RoomId?): ConfigureRoomPresenter } private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) @@ -122,6 +123,9 @@ class ConfigureRoomPresenter( } else { persistentListOf() } + + val parentSpace = spaces.find { it.roomId == initialParentSpaceId } + parentSpace?.let { dataStore.setParentSpace(it) } } LaunchedEffect(cameraPermissionState.permissionGranted) { diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 95134845e0..b2a0f1d5be 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -7,6 +7,8 @@ "Add name…" "New room" "New space" + "(no space)" + "Home" "Only people invited can join." "Private" "Anyone can find this room. diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt index a4d653fb63..d3150ea517 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt @@ -532,6 +532,7 @@ class ConfigureRoomPresenterTest { private fun createConfigureRoomPresenter( isSpace: Boolean = false, + initialParenSpaceId: RoomId? = null, roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(), dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper), matrixClient: MatrixClient = createMatrixClient(), @@ -543,6 +544,7 @@ class ConfigureRoomPresenterTest { mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), ) = ConfigureRoomPresenter( isSpace = isSpace, + initialParentSpaceId = initialParenSpaceId, dataStore = dataStore, matrixClient = matrixClient, mediaPickerProvider = pickerProvider, diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt index 5b7a6c1142..13f02e381a 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPointTest.kt @@ -14,6 +14,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import org.junit.Rule @@ -36,15 +37,16 @@ class DefaultCreateRoomEntryPointTest { plugins = plugins, ) } + val buildContext = BuildContext.root(null) + val callback = object : CreateRoomEntryPoint.Callback { override fun onRoomCreated(roomId: RoomId) = lambdaError() } - val result = entryPoint.createNode( - isSpace = false, - parentNode = parentNode, - buildContext = BuildContext.root(null), - callback = callback, - ) + val result = entryPoint + .builder(parentNode, buildContext, callback) + .setIsSpace(true) + .setParentSpace(A_ROOM_ID) + .build() assertThat(result.plugins).contains(callback) } } diff --git a/features/createroom/test/build.gradle.kts b/features/createroom/test/build.gradle.kts index 98aeac8a4c..afc98bdc0d 100644 --- a/features/createroom/test/build.gradle.kts +++ b/features/createroom/test/build.gradle.kts @@ -16,5 +16,6 @@ android { dependencies { implementation(projects.features.createroom.api) implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) implementation(projects.tests.testutils) } diff --git a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt index bbeb69c26b..a28e47e5a0 100644 --- a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt +++ b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/api/FakeCreateRoomEntryPoint.kt @@ -10,13 +10,19 @@ package io.element.android.features.createroom.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.tests.testutils.lambda.lambdaError class FakeCreateRoomEntryPoint : CreateRoomEntryPoint { - override fun createNode( - isSpace: Boolean, + class Builder : CreateRoomEntryPoint.Builder { + override fun setIsSpace(isSpace: Boolean): Builder = this + override fun setParentSpace(parentSpaceId: RoomId): Builder = this + override fun build(): Node = lambdaError() + } + + override fun builder( parentNode: Node, buildContext: BuildContext, callback: CreateRoomEntryPoint.Callback, - ): Node = lambdaError() + ): Builder = lambdaError() } diff --git a/features/space/impl/build.gradle.kts b/features/space/impl/build.gradle.kts index 03c8cda4ac..a29d388587 100644 --- a/features/space/impl/build.gradle.kts +++ b/features/space/impl/build.gradle.kts @@ -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) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 95d97156ff..666a538a29 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -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( 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(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() { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt index f56c3df8c2..5a5b10671c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -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 ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 9641e17625..5ad8feecc0 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -36,7 +36,7 @@ open class SpaceStateProvider : PreviewParameterProvider { 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 = emptySet(), joinActions: Map> = joiningRooms.associateWith { AsyncAction.Loading }, hideInvitesAvatar: Boolean = false, - hasMoreToLoad: Boolean = true, + hasMoreToLoad: Boolean = false, acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), topicViewerState: TopicViewerState = TopicViewerState.Hidden, canAccessSpaceSettings: Boolean = true, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 9f91e0f463..4618a83f3b 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -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 = {}, ) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt index 0e2717c055..0c3ae1b888 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt @@ -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 { diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 59041a97ed..ec4d681a40 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -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 { } + 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 AndroidComposeTestRule.setSpaceView( @@ -210,6 +228,7 @@ private fun AndroidComposeTestRule.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 AndroidComposeTestRule.setSpace onViewMembersClick = onViewMembersClick, onAddRoomClick = onAddRoomClick, acceptDeclineInviteView = acceptDeclineInviteView, + onCreateRoomClick = onCreateRoomClick, ) } } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt index c7d9aa4fd0..9cb8d48778 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/StartChatFlowNode.kt @@ -80,12 +80,14 @@ class StartChatFlowNode( navigator.onRoomCreated(roomId.toRoomIdOrAlias(), emptyList()) } } - createRoomEntryPoint.createNode( - isSpace = false, - parentNode = this, - buildContext = buildContext, - callback = callback, - ) + createRoomEntryPoint + .builder( + parentNode = this, + buildContext = buildContext, + callback = callback, + ) + .setIsSpace(false) + .build() } NavTarget.JoinByAddress -> { createNode(buildContext = buildContext, plugins = listOf(navigator)) diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png index f69878ffd5..8f42888316 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e59a9e2ae6ef36f28e61534b5639314cc840953df51bb1660e77e8d565865357 -size 32998 +oid sha256:6444cece1e3f5dfc94591380837fb8aa8caecdc4a87881485278c036accf2007 +size 40411 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png index 7f66fa5a85..06b582ecc5 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b854ce2b0618ebcbc88eff9952d6869bceeb8b43f9eccb8f8e5feef225e0a4c2 -size 33181 +oid sha256:70e98028aafc62aeb86e69db82dce9b9bb0250d47339a81e8e832a0600d21217 +size 40588 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png index b0f6f289a0..02e3dd1ecb 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1cf063ee5c9fbc50a53445050780ba239d4fb0fd1e9903a578eab8dd3bfc257 -size 33496 +oid sha256:a6a950c626b57f2624ce8929f61dfa2fcdc25036e0b3ebb5ec87a00e7531aea8 +size 40898 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png index 97dbb10528..08f5676c82 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:525059397001897f705630b8ac5a661439a502f2e623fdca252f9e86f97133e4 -size 58181 +oid sha256:778488da7192f2c3f98367d9d424783a42343a4bff8b7616f6175952526a8d30 +size 57766 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png index bc44bb4ce4..88df67a244 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59af097a2152b235c7df83aa76eea880ceef65edebebecb86f00745ec39712f4 -size 35937 +oid sha256:dbe04b9b4274183626f94d7014df9fc6462114226670161ffa3bb94b214b027e +size 34283 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png index 68f915afff..aff1fc2e08 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5151bbd3effcd6a46724a0cfb13730e845a3b8ba489826146c76d0a797403e0 -size 36502 +oid sha256:b86c4e7011f15079e01e7aa8e8c26b94cffb4ef451b3374d3834641336d3d036 +size 34853 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png index 9d406ab88a..8c1cbb8f74 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47c8bdf4d153ecebe749ae3cacfe4c9a1c59ac836106fb0903dca80305aedac2 -size 32412 +oid sha256:9b221b06e7533c21ed4393f388630c7119a1e7087b3a33de6c2cf498b01bbe65 +size 39544 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png index d178c45fec..347a57d0a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33917297cb1c6d38e5b955757de50f4ff6b73844bb87e8e88d0885daa01b266b -size 32556 +oid sha256:dd9b3d59af7fc710b7ef8f16ed961086693aa685de93658e2df85e3edbd7f787 +size 39689 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png index 35ca1a2937..02c60e8300 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:930f763051533ea3aa4032e483e15e0c1ccb1d213d2e59bebe7f934517b500ba -size 32868 +oid sha256:db27594444024da1818bbb089fac602b0937ad786072ad3542aa822e5a29c8c4 +size 40008 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png index 5f2f43a920..53bcb97254 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd673c2cf628285836848a732d3972a17421a2394464a3a77e7a49e4c5a862f1 -size 56636 +oid sha256:21ec87536d892719f5343c0aacde2ed4be1a7d0987eff72675b03b4be5038f78 +size 56278 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png index 3078485999..a734416241 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b459fa8aa195dcf85995ac51c2f079e9d2505efede138bcef94c0a5049e1f271 -size 35266 +oid sha256:8c01095702c3ab147349f9b60729b534ec1763bc5c04e43b6d571a52a115e10a +size 33634 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png index 7741306594..893ddc1840 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36fd123fcff07169723857c99e4e375fe9bbb8193b2a6e97508343ea025d5787 -size 35782 +oid sha256:ae1a096a6de24c2c6517455cd632291ad9d3d45e16df02fa75c33f278e44b26e +size 34149