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.features.joinroom.api.JoinRoomEntryPoint
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.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
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.RoomId
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.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@@ -72,6 +75,7 @@ class RoomFlowNode(
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
private val syncService: SyncService,
private val membershipObserver: RoomMembershipObserver,
private val spaceEntryPoint: SpaceEntryPoint,
) : BaseFlowNode<RoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Loading,
@@ -106,6 +110,9 @@ class RoomFlowNode(
@Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class Space(val spaceId: RoomId) : NavTarget
}
override fun onBuilt() {
@@ -146,17 +153,7 @@ class RoomFlowNode(
when (membership) {
CurrentUserMembership.JOINED -> {
if (isSpace) {
// It should not happen, but probably due to an issue in the sliding sync,
// 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,
)
)
backstack.newRoot(NavTarget.Space(spaceId = roomId))
} else {
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)
.callback(callback)
.params(params)
@@ -218,6 +215,11 @@ class RoomFlowNode(
)
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),
state = state.homeSpacesState,
onSpaceClick = { spaceId ->
// TODO
onRoomClick(spaceId)
}
)
}

View File

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

View File

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

View File

@@ -22,10 +22,11 @@ import io.element.android.libraries.di.SessionScope
class SpaceNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: SpacePresenter,
private val presenterFactory: SpacePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
val params = plugins.filterIsInstance<SpaceEntryPoint.Params>().single()
private val presenter = presenterFactory.create(params)
@Composable
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.getValue
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.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
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 seenInvitesStore: SeenInvitesStore,
) : Presenter<SpaceState> {
@AssistedFactory
interface Factory {
fun create(params: SpaceEntryPoint.Params): SpacePresenter
}
private val spaceRoomList = client.spaceService.spaceRoomList(params.roomId())
@Composable
override fun present(): SpaceState {
val hideInvitesAvatar by remember {
@@ -35,18 +46,19 @@ class SpacePresenter @Inject constructor(
.mediaPreviewConfigFlow
.mapState { config -> config.hideInviteAvatar }
}.collectAsState()
val spaceRooms by client.spaceService.spaceRoomsFlow.collectAsState(emptyList())
val seenSpaceInvites by remember {
seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
}.collectAsState(persistentSetOf())
val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList())
fun handleEvents(event: SpaceEvents) {
//when (event) { }
}
return SpaceState(
parentSpace = null,
children = spaceRooms.toPersistentList(),
children = children.toPersistentList(),
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
eventSink = ::handleEvents,

View File

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

View File

@@ -14,5 +14,5 @@ interface SpaceService {
val spaceRoomsFlow: SharedFlow<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.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList(
private val inner: InnerSpaceRoomList,
private val innerProvider: suspend () -> InnerSpaceRoomList,
sessionCoroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper,
) : SpaceRoomList {
private val inner = CompletableDeferred<InnerSpaceRoomList>()
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)
init {
inner.paginationStateFlow()
.onEach { paginationStatus ->
paginationStatusFlow.emit(paginationStatus.into())
}
.launchIn(sessionCoroutineScope)
sessionCoroutineScope.launch {
inner.complete(innerProvider())
}
sessionCoroutineScope.launch {
inner.await().paginationStateFlow()
.onEach { paginationStatus ->
paginationStatusFlow.emit(paginationStatus.into())
}
.collect()
}
inner.spaceListUpdateFlow()
.onEach { updates ->
spaceListUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
sessionCoroutineScope.launch {
inner.await().spaceListUpdateFlow()
.onEach { updates ->
spaceListUpdateProcessor.postUpdates(updates)
}
.collect()
}
}
override suspend fun paginate(): Result<Unit> {
return runCatchingExceptions {
inner.paginate()
inner.await().paginate()
}
}

View File

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

View File

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