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 fc8afab1bb
commit 6a9d084a50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 248 additions and 111 deletions

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