feature (space) : iterate on space list (and space screen)
This commit is contained in:
parent
2fe56f834f
commit
6b9afd9e6f
11 changed files with 79 additions and 44 deletions
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,6 @@ private fun SpaceViewTopBar(
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
},
|
},
|
||||||
windowInsets = WindowInsets(0.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue