diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index 90e111fc88..ab94ef719b 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -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 } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt index 768bd9c795..5c2cae4128 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -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, presenterFactory: SpacePresenter.Factory, private val matrixClient: MatrixClient, + private val acceptDeclineInviteView: AcceptDeclineInviteView, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun onOpenRoom(roomId: RoomId, viaParameters: List) @@ -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 ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index c709e9e5f3..1909926716 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -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, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @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>()) } + val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } 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>> + spaceRoom: SpaceRoom, + joinActions: Map>, + setJoinActions: (Map>) -> 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))) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index 5a629f94dd..ed6bc3dcf7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -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>, + val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (SpaceEvents) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 1f5feb1850..7b91da640f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -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 = 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 = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {} +) = AcceptDeclineInviteState( + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) + private fun aListOfSpaceRooms(): List { return listOf( aSpaceRoom( diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 87daa6a1a8..d3cbe7bb44 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -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 = {}, ) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 0204a05f0b..5845527223 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -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 = Presenter { anAcceptDeclineInviteState() }, ): SpacePresenter { return SpacePresenter( inputs = inputs, client = client, seenInvitesStore = seenInvitesStore, joinRoom = joinRoom, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, sessionCoroutineScope = backgroundScope, ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index 502b1b808c..7f4cab1baa 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -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 + } ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt index c1c2700889..38ffbfe2d3 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt @@ -18,9 +18,24 @@ class SpaceRoomProvider : PreviewParameterProvider { override val values: Sequence = 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,