feature (space) : iterate on space list (and space screen)

This commit is contained in:
ganfra 2025-09-08 21:57:25 +02:00 committed by Benoit Marty
parent 2fe56f834f
commit 6b9afd9e6f
11 changed files with 79 additions and 44 deletions

View file

@ -30,7 +30,9 @@ import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.appnav.room.joined.LoadingRoomNodeView import io.element.android.appnav.room.joined.LoadingRoomNodeView
import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
@ -43,6 +45,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
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.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@ -72,6 +75,7 @@ class RoomFlowNode(
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint, private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
private val syncService: SyncService, private val syncService: SyncService,
private val membershipObserver: RoomMembershipObserver, private val membershipObserver: RoomMembershipObserver,
private val spaceEntryPoint: SpaceEntryPoint,
) : BaseFlowNode<RoomFlowNode.NavTarget>( ) : BaseFlowNode<RoomFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Loading, initialElement = NavTarget.Loading,
@ -106,6 +110,9 @@ class RoomFlowNode(
@Parcelize @Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget data class JoinedRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class Space(val spaceId: RoomId) : NavTarget
} }
override fun onBuilt() { override fun onBuilt() {
@ -146,17 +153,7 @@ class RoomFlowNode(
when (membership) { when (membership) {
CurrentUserMembership.JOINED -> { CurrentUserMembership.JOINED -> {
if (isSpace) { if (isSpace) {
// It should not happen, but probably due to an issue in the sliding sync, backstack.newRoot(NavTarget.Space(spaceId = roomId))
// we can have a space here in case the space has just been joined.
// So navigate to the JoinRoom target for now, which will
// handle the space not supported screen
backstack.newRoot(
NavTarget.JoinRoom(
roomId = roomId,
serverNames = serverNames,
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
)
)
} else { } else {
backstack.newRoot(NavTarget.JoinedRoom(roomId)) backstack.newRoot(NavTarget.JoinedRoom(roomId))
} }
@ -194,7 +191,7 @@ class RoomFlowNode(
) )
} }
} }
val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias) val params = Params(navTarget.roomAlias)
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext) roomAliasResolverEntryPoint.nodeBuilder(this, buildContext)
.callback(callback) .callback(callback)
.params(params) .params(params)
@ -218,6 +215,11 @@ class RoomFlowNode(
) )
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
} }
is NavTarget.Space -> {
spaceEntryPoint.nodeBuilder(this, buildContext)
.params(SpaceEntryPoint.Params.Id(navTarget.spaceId))
.build()
}
} }
} }

View file

@ -269,7 +269,7 @@ private fun HomeScaffold(
.hazeSource(state = hazeState), .hazeSource(state = hazeState),
state = state.homeSpacesState, state = state.homeSpacesState,
onSpaceClick = { spaceId -> onSpaceClick = { spaceId ->
// TODO onRoomClick(spaceId)
} }
) )
} }

View file

@ -52,15 +52,15 @@ fun HomeSpacesView(
) )
} }
} }
state.spaceRooms.forEach { state.spaceRooms.forEach { spaceRoom ->
item(it.roomId) { item(spaceRoom.roomId) {
val isInvitation = it.state == CurrentUserMembership.INVITED val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
SpaceRoomItemView( SpaceRoomItemView(
spaceRoom = it, spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && it.roomId !in state.seenSpaceInvites, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar, hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = { onClick = {
onSpaceClick(it.roomId) onSpaceClick(spaceRoom.roomId)
}, },
onLongClick = { onLongClick = {

View file

@ -30,6 +30,13 @@ interface SpaceEntryPoint : FeatureEntryPoint {
sealed interface Params : Plugin { sealed interface Params : Plugin {
data class Id(val roomId: RoomId) : Params data class Id(val roomId: RoomId) : Params
data class Full(val spaceRoom: SpaceRoom) : Params data class Full(val spaceRoom: SpaceRoom) : Params
fun roomId(): RoomId {
return when (this) {
is Id -> roomId
is Full -> spaceRoom.roomId
}
}
} }
interface Callback : Plugin { interface Callback : Plugin {

View file

@ -22,10 +22,11 @@ import io.element.android.libraries.di.SessionScope
class SpaceNode @AssistedInject constructor( class SpaceNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: SpacePresenter, private val presenterFactory: SpacePresenter.Factory,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
val params = plugins.filterIsInstance<SpaceEntryPoint.Params>().single() val params = plugins.filterIsInstance<SpaceEntryPoint.Params>().single()
private val presenter = presenterFactory.create(params)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {

View file

@ -11,22 +11,33 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject
class SpacePresenter @Inject constructor( @Inject
class SpacePresenter(
@Assisted private val params: SpaceEntryPoint.Params,
private val client: MatrixClient, private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore, private val seenInvitesStore: SeenInvitesStore,
) : Presenter<SpaceState> { ) : Presenter<SpaceState> {
@AssistedFactory
interface Factory {
fun create(params: SpaceEntryPoint.Params): SpacePresenter
}
private val spaceRoomList = client.spaceService.spaceRoomList(params.roomId())
@Composable @Composable
override fun present(): SpaceState { override fun present(): SpaceState {
val hideInvitesAvatar by remember { val hideInvitesAvatar by remember {
@ -35,18 +46,19 @@ class SpacePresenter @Inject constructor(
.mediaPreviewConfigFlow .mediaPreviewConfigFlow
.mapState { config -> config.hideInviteAvatar } .mapState { config -> config.hideInviteAvatar }
}.collectAsState() }.collectAsState()
val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList())
val seenSpaceInvites by remember { val seenSpaceInvites by remember {
seenInvitesStore.seenRoomIds().map { it.toPersistentSet() } seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
}.collectAsState(persistentSetOf()) }.collectAsState(persistentSetOf())
val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList())
fun handleEvents(event: SpaceEvents) { fun handleEvents(event: SpaceEvents) {
//when (event) { } //when (event) { }
} }
return SpaceState( return SpaceState(
parentSpace = null, parentSpace = null,
children = spaceRooms.toPersistentList(), children = children.toPersistentList(),
seenSpaceInvites = seenSpaceInvites, seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar, hideInvitesAvatar = hideInvitesAvatar,
eventSink = ::handleEvents, eventSink = ::handleEvents,

View file

@ -132,7 +132,6 @@ private fun SpaceViewTopBar(
}, },
actions = { actions = {
}, },
windowInsets = WindowInsets(0.dp)
) )
} }

View file

@ -14,5 +14,5 @@ interface SpaceService {
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>> val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
suspend fun joinedSpaces(): Result<List<SpaceRoom>> suspend fun joinedSpaces(): Result<List<SpaceRoom>>
suspend fun spaceRoomList(id: RoomId): SpaceRoomList fun spaceRoomList(id: RoomId): SpaceRoomList
} }

View file

@ -10,40 +10,52 @@ package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList( class RustSpaceRoomList(
private val inner: InnerSpaceRoomList, private val innerProvider: suspend () -> InnerSpaceRoomList,
sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper, spaceRoomMapper: SpaceRoomMapper,
) : SpaceRoomList { ) : SpaceRoomList {
private val inner = CompletableDeferred<InnerSpaceRoomList>()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = Int.MAX_VALUE) override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
override val paginationStatusFlow = MutableStateFlow(inner.paginationState().into()) override val paginationStatusFlow: MutableStateFlow<SpaceRoomList.PaginationStatus> =
MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper) private val spaceListUpdateProcessor = SpaceListUpdateProcessor(spaceRoomsFlow, spaceRoomMapper)
init { init {
inner.paginationStateFlow() sessionCoroutineScope.launch {
.onEach { paginationStatus -> inner.complete(innerProvider())
paginationStatusFlow.emit(paginationStatus.into()) }
} sessionCoroutineScope.launch {
.launchIn(sessionCoroutineScope) inner.await().paginationStateFlow()
.onEach { paginationStatus ->
paginationStatusFlow.emit(paginationStatus.into())
}
.collect()
}
inner.spaceListUpdateFlow() sessionCoroutineScope.launch {
.onEach { updates -> inner.await().spaceListUpdateFlow()
spaceListUpdateProcessor.postUpdates(updates) .onEach { updates ->
} spaceListUpdateProcessor.postUpdates(updates)
.launchIn(sessionCoroutineScope) }
.collect()
}
} }
override suspend fun paginate(): Result<Unit> { override suspend fun paginate(): Result<Unit> {
return runCatchingExceptions { return runCatchingExceptions {
inner.paginate() inner.await().paginate()
} }
} }

View file

@ -51,10 +51,9 @@ class RustSpaceService(
} }
} }
override suspend fun spaceRoomList(id: RoomId): SpaceRoomList { override fun spaceRoomList(id: RoomId): SpaceRoomList {
val innerSpaceRoomList = innerSpaceService.spaceRoomList(id.value)
return RustSpaceRoomList( return RustSpaceRoomList(
inner = innerSpaceRoomList, innerProvider = { innerSpaceService.spaceRoomList(id.value) },
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
spaceRoomMapper = spaceRoomMapper spaceRoomMapper = spaceRoomMapper
) )

View file

@ -28,6 +28,9 @@ internal fun SpaceRoomListInterface.paginationStateFlow(): Flow<SpaceRoomListPag
trySend(paginationState) trySend(paginationState)
} }
} }
// Send the initial value
trySend(paginationState())
// Then subscribe to updates
val result = subscribeToPaginationStateUpdates(listener) val result = subscribeToPaginationStateUpdates(listener)
awaitClose { awaitClose {
result.cancelAndDestroy() result.cancelAndDestroy()