feature (space) : allow joining children from space screen

This commit is contained in:
ganfra
2025-09-26 11:25:08 +02:00
parent 7050076beb
commit 8f0841673c
8 changed files with 95 additions and 9 deletions

View File

@@ -64,7 +64,7 @@ fun HomeSpacesView(
},
onLongClick = {
// TODO
}
},
)
}
}

View File

@@ -7,6 +7,9 @@
package io.element.android.features.space.impl
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
sealed interface SpaceEvents {
data object LoadMore : SpaceEvents
data class Join(val spaceRoom: SpaceRoom): SpaceEvents
}

View File

@@ -9,18 +9,27 @@ package io.element.android.features.space.impl
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
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.JoinedRoom
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.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentSetOf
@@ -31,14 +40,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Inject
class SpacePresenter(
@Inject class SpacePresenter(
@Assisted private val inputs: SpaceEntryPoint.Inputs,
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
private val joinRoom: JoinRoom,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
) : Presenter<SpaceState> {
@AssistedFactory
fun interface Factory {
@AssistedFactory fun interface Factory {
fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter
}
@@ -54,7 +63,7 @@ class SpacePresenter(
seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
}.collectAsState(persistentSetOf())
val coroutineScope = rememberCoroutineScope()
val localCoroutineScope = rememberCoroutineScope()
val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList())
val hasMoreToLoad by remember {
spaceRoomList.paginationStatusFlow.mapState { status ->
@@ -66,10 +75,24 @@ class SpacePresenter(
}.collectAsState()
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val joiningRooms = remember { mutableStateOf(emptySet<RoomId>()) }
LaunchedEffect(children, joiningRooms.value) {
val joinedChildren = children
.filter { it.state == CurrentUserMembership.JOINED }
.map { it.roomId }
.toSet()
joiningRooms.value.let { currentlyJoining ->
joiningRooms.value = currentlyJoining - joinedChildren
}
}
fun handleEvents(event: SpaceEvents) {
when (event) {
SpaceEvents.LoadMore -> coroutineScope.paginate()
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
is SpaceEvents.Join -> {
sessionCoroutineScope.joinRoom(event.spaceRoom, joiningRooms)
}
}
}
return SpaceState(
@@ -78,10 +101,24 @@ class SpacePresenter(
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
joiningRooms = joiningRooms.value.toPersistentSet(),
eventSink = ::handleEvents,
)
}
private fun CoroutineScope.joinRoom(
spaceRoom: SpaceRoom, joiningRooms: MutableState<Set<RoomId>>
) = launch {
joiningRooms.value = joiningRooms.value + spaceRoom.roomId
joinRoom.invoke(
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
serverNames = spaceRoom.via,
trigger = JoinedRoom.Trigger.SpaceHierarchy,
).onFailure {
joiningRooms.value = joiningRooms.value - spaceRoom.roomId
}
}
private fun CoroutineScope.paginate() = launch {
spaceRoomList.paginate()
}

View File

@@ -18,5 +18,6 @@ data class SpaceState(
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val hasMoreToLoad: Boolean,
val joiningRooms: ImmutableSet<RoomId>,
val eventSink: (SpaceEvents) -> Unit
)

View File

@@ -47,6 +47,7 @@ fun aSpaceState(
),
children: List<SpaceRoom> = emptyList(),
seenSpaceInvites: Set<RoomId> = emptySet(),
joiningRooms: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
hasMoreToLoad: Boolean = false,
) = SpaceState(
@@ -55,6 +56,7 @@ fun aSpaceState(
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
joiningRooms = joiningRooms.toImmutableSet(),
eventSink = {})
private fun aListOfSpaceRooms(): List<SpaceRoom> {

View File

@@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
@@ -35,9 +37,11 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
@@ -96,6 +100,7 @@ private fun SpaceViewContent(
state.children.forEach { spaceRoom ->
item {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
val isCurrentlyJoining = spaceRoom.roomId in state.joiningRooms
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
@@ -105,6 +110,9 @@ private fun SpaceViewContent(
},
onLongClick = {
// TODO
},
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
state.eventSink(SpaceEvents.Join(spaceRoom))
}
)
}
@@ -191,6 +199,27 @@ private fun SpaceAvatarAndNameRow(
}
}
private fun SpaceRoom.trailingAction(
isCurrentlyJoining: Boolean,
onClick: () -> Unit
): @Composable (() -> Unit)? {
return when (state) {
null, CurrentUserMembership.LEFT -> {
@Composable {
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionAccent) {
TextButton(
text = stringResource(CommonStrings.action_join),
onClick = onClick,
size = ButtonSize.LargeLowPadding,
showProgress = isCurrentlyJoining,
)
}
}
}
else -> null
}
}
@PreviewsDayNight
@Composable
internal fun SpaceViewPreview(

View File

@@ -15,10 +15,12 @@ import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -27,7 +29,7 @@ class DefaultSpaceEntryPointTest {
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun `test node builder`() {
fun `test node builder`() = runTest {
val entryPoint = DefaultSpaceEntryPoint()
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
val parentNode = TestParentNode.create { buildContext, plugins ->
@@ -44,6 +46,10 @@ class DefaultSpaceEntryPointTest {
)
),
seenInvitesStore = InMemorySeenInvitesStore(),
joinRoom = FakeJoinRoom(
lambda = { _, _, _ -> Result.success(Unit) },
),
sessionCoroutineScope = backgroundScope,
)
},
)

View File

@@ -14,15 +14,18 @@ import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.space.api.SpaceEntryPoint
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
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -153,15 +156,20 @@ class SpacePresenterTest {
}
}
private fun createSpacePresenter(
private fun TestScope.createSpacePresenter(
inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID),
client: MatrixClient = FakeMatrixClient(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
joinRoom: JoinRoom = FakeJoinRoom(
lambda = { _, _, _ -> Result.success(Unit) },
),
): SpacePresenter {
return SpacePresenter(
inputs = inputs,
client = client,
seenInvitesStore = seenInvitesStore,
joinRoom = joinRoom,
sessionCoroutineScope = backgroundScope,
)
}
}