feature (space) : allow joining children from space screen
This commit is contained in:
@@ -64,7 +64,7 @@ fun HomeSpacesView(
|
||||
},
|
||||
onLongClick = {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user