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:
Jorge Martin Espinosa
2026-01-27 11:12:12 +01:00
committed by GitHub
parent cc4bf95cac
commit 4b4492681b
31 changed files with 248 additions and 111 deletions

View File

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

View File

@@ -38,7 +38,7 @@ class CreateRoomFlowNode(
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.ConfigureRoom(isSpace = plugins.filterIsInstance<Inputs>().first().isSpace),
initialElement = initialElementFromInputs(plugins.filterIsInstance<Inputs>().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,
)

View File

@@ -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<CreateRoomFlowNode>(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<CreateRoomFlowNode>(buildContext, listOf(inputs, callback))
}
}
override fun builder(parentNode: Node, buildContext: BuildContext, callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.Builder {
return Builder(parentNode, buildContext, callback)
}
}

View File

@@ -42,11 +42,12 @@ class ConfigureRoomNode(
@Parcelize
data class Inputs(
val isSpace: Boolean,
val parentSpaceId: RoomId?,
) : NodeInputs, Parcelable
private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.isSpace)
private val presenter = presenterFactory.create(inputs.isSpace, inputs.parentSpaceId)
init {
lifecycle.subscribe(

View File

@@ -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<ConfigureRoomState> {
@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) {

View File

@@ -7,6 +7,8 @@
<string name="screen_create_room_name_placeholder">"Add name…"</string>
<string name="screen_create_room_new_room_title">"New room"</string>
<string name="screen_create_room_new_space_title">"New space"</string>
<string name="screen_create_room_parent_space_home_description">"(no space)"</string>
<string name="screen_create_room_parent_space_home_title">"Home"</string>
<string name="screen_create_room_private_option_description">"Only people invited can join."</string>
<string name="screen_create_room_private_option_title">"Private"</string>
<string name="screen_create_room_public_option_description">"Anyone can find this room.

View File

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

View File

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

View File

@@ -16,5 +16,6 @@ android {
dependencies {
implementation(projects.features.createroom.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}

View File

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