feature (space) : allow joining children from space screen
This commit is contained in:
parent
7050076beb
commit
8f0841673c
8 changed files with 95 additions and 9 deletions
|
|
@ -64,7 +64,7 @@ fun HomeSpacesView(
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl
|
package io.element.android.features.space.impl
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,27 @@ package io.element.android.features.space.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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 dev.zacsweers.metro.Inject
|
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.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.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.matrix.api.MatrixClient
|
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.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
|
||||||
|
|
@ -31,14 +40,14 @@ import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
@Inject
|
@Inject 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,
|
||||||
|
private val joinRoom: JoinRoom,
|
||||||
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<SpaceState> {
|
) : Presenter<SpaceState> {
|
||||||
@AssistedFactory
|
@AssistedFactory fun interface Factory {
|
||||||
fun interface Factory {
|
|
||||||
fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter
|
fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +63,7 @@ class SpacePresenter(
|
||||||
seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
|
seenInvitesStore.seenRoomIds().map { it.toPersistentSet() }
|
||||||
}.collectAsState(persistentSetOf())
|
}.collectAsState(persistentSetOf())
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList())
|
val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList())
|
||||||
val hasMoreToLoad by remember {
|
val hasMoreToLoad by remember {
|
||||||
spaceRoomList.paginationStatusFlow.mapState { status ->
|
spaceRoomList.paginationStatusFlow.mapState { status ->
|
||||||
|
|
@ -66,10 +75,24 @@ class SpacePresenter(
|
||||||
}.collectAsState()
|
}.collectAsState()
|
||||||
|
|
||||||
val currentSpace by spaceRoomList.currentSpaceFlow.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) {
|
fun handleEvents(event: SpaceEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
SpaceEvents.LoadMore -> coroutineScope.paginate()
|
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
|
||||||
|
is SpaceEvents.Join -> {
|
||||||
|
sessionCoroutineScope.joinRoom(event.spaceRoom, joiningRooms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SpaceState(
|
return SpaceState(
|
||||||
|
|
@ -78,10 +101,24 @@ class SpacePresenter(
|
||||||
seenSpaceInvites = seenSpaceInvites,
|
seenSpaceInvites = seenSpaceInvites,
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
|
joiningRooms = joiningRooms.value.toPersistentSet(),
|
||||||
eventSink = ::handleEvents,
|
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 {
|
private fun CoroutineScope.paginate() = launch {
|
||||||
spaceRoomList.paginate()
|
spaceRoomList.paginate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,6 @@ 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 eventSink: (SpaceEvents) -> Unit
|
val eventSink: (SpaceEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ fun aSpaceState(
|
||||||
),
|
),
|
||||||
children: List<SpaceRoom> = emptyList(),
|
children: List<SpaceRoom> = emptyList(),
|
||||||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||||
|
joiningRooms: Set<RoomId> = emptySet(),
|
||||||
hideInvitesAvatar: Boolean = false,
|
hideInvitesAvatar: Boolean = false,
|
||||||
hasMoreToLoad: Boolean = false,
|
hasMoreToLoad: Boolean = false,
|
||||||
) = SpaceState(
|
) = SpaceState(
|
||||||
|
|
@ -55,6 +56,7 @@ fun aSpaceState(
|
||||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
|
joiningRooms = joiningRooms.toImmutableSet(),
|
||||||
eventSink = {})
|
eventSink = {})
|
||||||
|
|
||||||
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
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.layout.padding
|
||||||
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
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.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
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.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
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.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
|
|
@ -96,6 +100,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
|
||||||
SpaceRoomItemView(
|
SpaceRoomItemView(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||||
|
|
@ -105,6 +110,9 @@ private fun SpaceViewContent(
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
// TODO
|
// 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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SpaceViewPreview(
|
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.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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.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.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -27,7 +29,7 @@ class DefaultSpaceEntryPointTest {
|
||||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test node builder`() {
|
fun `test node builder`() = runTest {
|
||||||
val entryPoint = DefaultSpaceEntryPoint()
|
val entryPoint = DefaultSpaceEntryPoint()
|
||||||
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
||||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||||
|
|
@ -44,6 +46,10 @@ class DefaultSpaceEntryPointTest {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
seenInvitesStore = InMemorySeenInvitesStore(),
|
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.invite.test.InMemorySeenInvitesStore
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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.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.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
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),
|
inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID),
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||||
|
joinRoom: JoinRoom = FakeJoinRoom(
|
||||||
|
lambda = { _, _, _ -> Result.success(Unit) },
|
||||||
|
),
|
||||||
): SpacePresenter {
|
): SpacePresenter {
|
||||||
return SpacePresenter(
|
return SpacePresenter(
|
||||||
inputs = inputs,
|
inputs = inputs,
|
||||||
client = client,
|
client = client,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
|
joinRoom = joinRoom,
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue