feature (space) : handle accept decline invite

This commit is contained in:
ganfra
2025-09-30 15:59:29 +02:00
parent de4e3d8735
commit dbffad29d0
9 changed files with 135 additions and 30 deletions

View File

@@ -13,4 +13,6 @@ sealed interface SpaceEvents {
data object LoadMore : SpaceEvents
data class Join(val spaceRoom: SpaceRoom) : SpaceEvents
data object ClearFailures : SpaceEvents
data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents
data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents
}

View File

@@ -18,6 +18,7 @@ import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.androidutils.R
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
@@ -36,6 +37,7 @@ class SpaceNode(
@Assisted plugins: List<Plugin>,
presenterFactory: SpacePresenter.Factory,
private val matrixClient: MatrixClient,
private val acceptDeclineInviteView: AcceptDeclineInviteView,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
@@ -79,6 +81,18 @@ class SpaceNode(
onShareSpace = {
onShareRoom(context)
},
acceptDeclineInviteView = {
acceptDeclineInviteView.Render(
state = state.acceptDeclineInviteState,
onAcceptInviteSuccess = { roomId ->
callback.onOpenRoom(roomId, emptyList())
},
onDeclineInviteSuccess = { roomId ->
// No action needed
},
modifier = Modifier
)
},
modifier = modifier
)
}

View File

@@ -9,7 +9,6 @@ package io.element.android.features.space.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -20,6 +19,9 @@ import dev.zacsweers.metro.AssistedFactory
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.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
@@ -47,6 +49,7 @@ import kotlin.jvm.optionals.getOrNull
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
private val joinRoom: JoinRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
) : Presenter<SpaceState> {
@AssistedFactory fun interface Factory {
@@ -77,26 +80,39 @@ import kotlin.jvm.optionals.getOrNull
}.collectAsState()
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val joinActions = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
LaunchedEffect(children) {
val joinedChildren = children.filter { it.state == CurrentUserMembership.JOINED }.map { it.roomId }.toSet()
joinActions.value.let { currentlyJoining ->
joinActions.value = currentlyJoining - joinedChildren
}
// Remove joined children from the join actions
val joinedChildren = children
.filter { it.state == CurrentUserMembership.JOINED }
.map { it.roomId }
setJoinActions(joinActions - joinedChildren)
}
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
fun handleEvents(event: SpaceEvents) {
when (event) {
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
is SpaceEvents.Join -> {
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions)
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions)
}
SpaceEvents.ClearFailures -> {
val failedActions = joinActions.value
val failedActions = joinActions
.filterValues { it is AsyncAction.Failure }
.mapValues { AsyncAction.Uninitialized }
joinActions.value = joinActions.value + failedActions
setJoinActions(joinActions + failedActions)
}
is SpaceEvents.AcceptInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(event.spaceRoom.toInviteData())
)
}
is SpaceEvents.DeclineInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false)
)
}
}
}
@@ -106,21 +122,24 @@ import kotlin.jvm.optionals.getOrNull
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
joinActions = joinActions.value.toPersistentMap(),
joinActions = joinActions.toPersistentMap(),
acceptDeclineInviteState = acceptDeclineInviteState,
eventSink = ::handleEvents,
)
}
private fun CoroutineScope.joinRoom(
spaceRoom: SpaceRoom, joiningRooms: MutableState<Map<RoomId, AsyncAction<Unit>>>
spaceRoom: SpaceRoom,
joinActions: Map<RoomId, AsyncAction<Unit>>,
setJoinActions: (Map<RoomId, AsyncAction<Unit>>) -> Unit
) = launch {
joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Loading)
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Loading))
joinRoom.invoke(
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
serverNames = spaceRoom.via,
trigger = JoinedRoom.Trigger.SpaceHierarchy,
).onFailure {
joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Failure(it))
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)))
}
}

View File

@@ -7,6 +7,7 @@
package io.element.android.features.space.impl.root
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
@@ -21,6 +22,7 @@ data class SpaceState(
val hideInvitesAvatar: Boolean,
val hasMoreToLoad: Boolean,
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val eventSink: (SpaceEvents) -> Unit
) {
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading

View File

@@ -8,6 +8,8 @@
package io.element.android.features.space.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@@ -54,6 +56,7 @@ fun aSpaceState(
joiningRooms: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
hasMoreToLoad: Boolean = false,
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
) = SpaceState(
currentSpace = parentSpace,
children = children.toImmutableList(),
@@ -61,9 +64,20 @@ fun aSpaceState(
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(),
acceptDeclineInviteState = acceptDeclineInviteState,
eventSink = {}
)
internal fun anAcceptDeclineInviteState(
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
) = AcceptDeclineInviteState(
acceptAction = acceptAction,
declineAction = declineAction,
eventSink = eventSink,
)
private fun aListOfSpaceRooms(): List<SpaceRoom> {
return listOf(
aSpaceRoom(

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
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
@@ -71,6 +72,7 @@ fun SpaceView(
onShareSpace: () -> Unit,
onLeaveSpaceClick: () -> Unit,
modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit,
) {
Scaffold(
modifier = modifier,
@@ -94,6 +96,7 @@ fun SpaceView(
hasAnyFailure = state.hasAnyFailure,
eventSink = state.eventSink
)
acceptDeclineInviteView()
}
},
)
@@ -157,7 +160,15 @@ private fun SpaceViewContent(
},
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
state.eventSink(SpaceEvents.Join(spaceRoom))
}
},
bottomAction = spaceRoom.inviteButtons(
onAcceptClick = {
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
},
onDeclineClick = {
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
}
)
)
}
}
@@ -311,6 +322,23 @@ private fun SpaceRoom.trailingAction(
}
}
private fun SpaceRoom.inviteButtons(
onAcceptClick: () -> Unit,
onDeclineClick: () -> Unit,
): @Composable (() -> Unit)? {
return when (state) {
CurrentUserMembership.INVITED -> {
@Composable {
InviteButtonsRowMolecule(
onAcceptClick = onAcceptClick,
onDeclineClick = onDeclineClick,
)
}
}
else -> null
}
}
@PreviewsDayNight
@Composable
internal fun SpaceViewPreview(
@@ -321,6 +349,7 @@ internal fun SpaceViewPreview(
onRoomClick = {},
onShareSpace = {},
onLeaveSpaceClick = {},
acceptDeclineInviteView = {},
onBackClick = {},
)
}

View File

@@ -11,8 +11,10 @@ package io.element.android.features.space.impl.root
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
@@ -163,12 +165,14 @@ class SpacePresenterTest {
joinRoom: JoinRoom = FakeJoinRoom(
lambda = { _, _, _ -> Result.success(Unit) },
),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
): SpacePresenter {
return SpacePresenter(
inputs = inputs,
client = client,
seenInvitesStore = seenInvitesStore,
joinRoom = joinRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
sessionCoroutineScope = backgroundScope,
)
}

View File

@@ -63,6 +63,7 @@ fun SpaceRoomItemView(
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
trailingAction: @Composable (() -> Unit)? = null,
bottomAction: @Composable (() -> Unit)? = null,
) {
SpaceRoomItemScaffold(
modifier = modifier,
@@ -84,22 +85,22 @@ fun SpaceRoomItemView(
subtitle = spaceRoom.subtitle()
)
Spacer(modifier = Modifier.height(1.dp))
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = spaceRoom.info(),
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (spaceRoom.state == CurrentUserMembership.INVITED) {
Spacer(modifier = Modifier.height(12.dp))
InviteButtonsRowMolecule(
onAcceptClick = {},
onDeclineClick = {},
val info = spaceRoom.info()
if (info.isNotBlank()) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = info,
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (bottomAction != null) {
Spacer(modifier = Modifier.height(12.dp))
bottomAction()
}
}
}
@@ -253,6 +254,11 @@ internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class
hideAvatars = false,
onClick = {},
onLongClick = {},
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) {
{ InviteButtonsRowMolecule({}, {}) }
} else {
null
}
)
}

View File

@@ -18,9 +18,24 @@ class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
override val values: Sequence<SpaceRoom> = sequenceOf(
aSpaceRoom(
roomType = RoomType.Room,
name = "Room name",
name = "Room name with topic",
topic = "Room topic that is quite long and might be truncated"
),
aSpaceRoom(
roomType = RoomType.Room,
name = "Room name no topic",
),
aSpaceRoom(
roomType = RoomType.Room,
name = "Room name with topic",
topic = "Room topic that is quite long and might be truncated",
state = CurrentUserMembership.INVITED,
),
aSpaceRoom(
roomType = RoomType.Room,
name = "Room name no topic",
state = CurrentUserMembership.INVITED,
),
aSpaceRoom(
numJoinedMembers = 5,
childrenCount = 10,