feature (space) : manage failures to join in Space screen
This commit is contained in:
parent
0390fde615
commit
526bc27a08
5 changed files with 70 additions and 28 deletions
|
|
@ -12,4 +12,5 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
sealed interface SpaceEvents {
|
sealed interface SpaceEvents {
|
||||||
data object LoadMore : SpaceEvents
|
data object LoadMore : SpaceEvents
|
||||||
data class Join(val spaceRoom: SpaceRoom): SpaceEvents
|
data class Join(val spaceRoom: SpaceRoom): SpaceEvents
|
||||||
|
data object ClearFailures : SpaceEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,11 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
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.features.space.api.SpaceEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
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.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
|
|
@ -34,14 +35,14 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject class SpacePresenter(
|
||||||
class SpacePresenter(
|
|
||||||
@Assisted private val inputs: SpaceEntryPoint.Inputs,
|
@Assisted private val inputs: SpaceEntryPoint.Inputs,
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
|
@ -76,15 +77,12 @@ class SpacePresenter(
|
||||||
}.collectAsState()
|
}.collectAsState()
|
||||||
|
|
||||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
||||||
val joiningRooms = remember { mutableStateOf(emptySet<RoomId>()) }
|
val joinActions = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
|
||||||
|
|
||||||
LaunchedEffect(children, joiningRooms.value) {
|
LaunchedEffect(children) {
|
||||||
val joinedChildren = children
|
val joinedChildren = children.filter { it.state == CurrentUserMembership.JOINED }.map { it.roomId }.toSet()
|
||||||
.filter { it.state == CurrentUserMembership.JOINED }
|
joinActions.value.let { currentlyJoining ->
|
||||||
.map { it.roomId }
|
joinActions.value = currentlyJoining - joinedChildren
|
||||||
.toSet()
|
|
||||||
joiningRooms.value.let { currentlyJoining ->
|
|
||||||
joiningRooms.value = currentlyJoining - joinedChildren
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +90,13 @@ class SpacePresenter(
|
||||||
when (event) {
|
when (event) {
|
||||||
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
|
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
|
||||||
is SpaceEvents.Join -> {
|
is SpaceEvents.Join -> {
|
||||||
sessionCoroutineScope.joinRoom(event.spaceRoom, joiningRooms)
|
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions)
|
||||||
|
}
|
||||||
|
SpaceEvents.ClearFailures -> {
|
||||||
|
val failedActions = joinActions.value
|
||||||
|
.filterValues { it is AsyncAction.Failure }
|
||||||
|
.mapValues { AsyncAction.Uninitialized }
|
||||||
|
joinActions.value = joinActions.value + failedActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,21 +106,21 @@ class SpacePresenter(
|
||||||
seenSpaceInvites = seenSpaceInvites,
|
seenSpaceInvites = seenSpaceInvites,
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
joiningRooms = joiningRooms.value.toPersistentSet(),
|
joinActions = joinActions.value.toPersistentMap(),
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.joinRoom(
|
private fun CoroutineScope.joinRoom(
|
||||||
spaceRoom: SpaceRoom, joiningRooms: MutableState<Set<RoomId>>
|
spaceRoom: SpaceRoom, joiningRooms: MutableState<Map<RoomId, AsyncAction<Unit>>>
|
||||||
) = launch {
|
) = launch {
|
||||||
joiningRooms.value = joiningRooms.value + spaceRoom.roomId
|
joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Loading)
|
||||||
joinRoom.invoke(
|
joinRoom.invoke(
|
||||||
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
|
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
|
||||||
serverNames = spaceRoom.via,
|
serverNames = spaceRoom.via,
|
||||||
trigger = JoinedRoom.Trigger.SpaceHierarchy,
|
trigger = JoinedRoom.Trigger.SpaceHierarchy,
|
||||||
).onFailure {
|
).onFailure {
|
||||||
joiningRooms.value = joiningRooms.value - spaceRoom.roomId
|
joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Failure(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.root
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
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.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
data class SpaceState(
|
data class SpaceState(
|
||||||
|
|
@ -18,6 +20,11 @@ data class SpaceState(
|
||||||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||||
val hideInvitesAvatar: Boolean,
|
val hideInvitesAvatar: Boolean,
|
||||||
val hasMoreToLoad: Boolean,
|
val hasMoreToLoad: Boolean,
|
||||||
val joiningRooms: ImmutableSet<RoomId>,
|
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
|
||||||
val eventSink: (SpaceEvents) -> Unit
|
val eventSink: (SpaceEvents) -> Unit
|
||||||
)
|
) {
|
||||||
|
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
||||||
|
val hasAnyFailure: Boolean = joinActions.values.any {
|
||||||
|
it is AsyncAction.Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.space.impl
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
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.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
|
||||||
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
|
|
@ -56,7 +58,7 @@ fun aSpaceState(
|
||||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
joiningRooms = joiningRooms.toImmutableSet(),
|
joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(),
|
||||||
eventSink = {})
|
eventSink = {})
|
||||||
|
|
||||||
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
|
@ -34,6 +35,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||||
|
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -58,22 +62,22 @@ import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpaceView(
|
fun SpaceView(
|
||||||
state: SpaceState,
|
state: SpaceState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onLeaveSpaceClick: () -> Unit,
|
|
||||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
|
onLeaveSpaceClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
SpaceViewTopBar(
|
SpaceViewTopBar(
|
||||||
state = state,
|
currentSpace = state.currentSpace, onBackClick = onBackClick,
|
||||||
onBackClick = onBackClick,
|
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
onShareSpace = onShareSpace,
|
onShareSpace = onShareSpace,
|
||||||
)
|
)
|
||||||
|
|
@ -89,6 +93,30 @@ fun SpaceView(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
JoinRoomFailureEffect(
|
||||||
|
hasAnyFailure = state.hasAnyFailure,
|
||||||
|
eventSink = state.eventSink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun JoinRoomFailureEffect(
|
||||||
|
hasAnyFailure: Boolean,
|
||||||
|
eventSink: (SpaceEvents) -> Unit,
|
||||||
|
) {
|
||||||
|
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||||
|
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState)
|
||||||
|
LaunchedEffect(hasAnyFailure) {
|
||||||
|
if (hasAnyFailure) {
|
||||||
|
asyncIndicatorState.enqueue {
|
||||||
|
AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong))
|
||||||
|
}
|
||||||
|
delay(AsyncIndicator.DURATION_SHORT)
|
||||||
|
eventSink(SpaceEvents.ClearFailures)
|
||||||
|
} else {
|
||||||
|
asyncIndicatorState.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -115,7 +143,7 @@ private fun SpaceViewContent(
|
||||||
state.children.forEach { spaceRoom ->
|
state.children.forEach { spaceRoom ->
|
||||||
item {
|
item {
|
||||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||||
val isCurrentlyJoining = spaceRoom.roomId in state.joiningRooms
|
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||||
SpaceRoomItemView(
|
SpaceRoomItemView(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||||
|
|
@ -163,13 +191,12 @@ private fun LoadingMoreIndicator(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpaceViewTopBar(
|
private fun SpaceViewTopBar(
|
||||||
state: SpaceState,
|
currentSpace: SpaceRoom?,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val currentSpace = state.currentSpace
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|
@ -229,6 +256,7 @@ private fun SpaceViewTopBar(
|
||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -290,9 +318,9 @@ internal fun SpaceViewPreview(
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
SpaceView(
|
SpaceView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = {},
|
|
||||||
onLeaveSpaceClick = {},
|
|
||||||
onRoomClick = {},
|
onRoomClick = {},
|
||||||
onShareSpace = {},
|
onShareSpace = {},
|
||||||
|
onLeaveSpaceClick = {},
|
||||||
|
onBackClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue