Merge pull request #5431 from element-hq/feature/fga/space_list_join_action
Feature : space list join action
This commit is contained in:
commit
9ba549f08e
42 changed files with 829 additions and 80 deletions
|
|
@ -16,14 +16,12 @@ import io.element.android.features.home.impl.model.aRoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.model.anInviteSender
|
import io.element.android.features.home.impl.model.anInviteSender
|
||||||
import io.element.android.features.home.impl.search.RoomListSearchState
|
import io.element.android.features.home.impl.search.RoomListSearchState
|
||||||
import io.element.android.features.home.impl.search.aRoomListSearchState
|
import io.element.android.features.home.impl.search.aRoomListSearchState
|
||||||
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.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -76,16 +74,6 @@ internal fun aLeaveRoomState(
|
||||||
override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
|
override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun anAcceptDeclineInviteState(
|
|
||||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
|
||||||
) = AcceptDeclineInviteState(
|
|
||||||
acceptAction = acceptAction,
|
|
||||||
declineAction = declineAction,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||||
return persistentListOf(
|
return persistentListOf(
|
||||||
aRoomListRoomSummary(
|
aRoomListRoomSummary(
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ fun HomeSpacesView(
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.home.impl.search.aRoomListSearchState
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
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.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.api.acceptdecline
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
fun anAcceptDeclineInviteState(
|
||||||
|
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
|
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (AcceptDeclineInviteEvents) -> Unit = {},
|
||||||
|
) = AcceptDeclineInviteState(
|
||||||
|
acceptAction = acceptAction,
|
||||||
|
declineAction = declineAction,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -9,9 +9,9 @@ package io.element.android.features.invite.impl.acceptdecline
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invite.api.InviteData
|
import io.element.android.features.invite.api.InviteData
|
||||||
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.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
|
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.impl.AcceptInvite
|
import io.element.android.features.invite.impl.AcceptInvite
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -51,13 +51,3 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun anAcceptDeclineInviteState(
|
|
||||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
|
||||||
) = AcceptDeclineInviteState(
|
|
||||||
acceptAction = acceptAction,
|
|
||||||
declineAction = declineAction,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invite.api.InviteData
|
import io.element.android.features.invite.api.InviteData
|
||||||
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.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -219,16 +219,6 @@ fun aJoinRoomState(
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun anAcceptDeclineInviteState(
|
|
||||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
|
|
||||||
) = AcceptDeclineInviteState(
|
|
||||||
acceptAction = acceptAction,
|
|
||||||
declineAction = declineAction,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun anInviteSender(
|
internal fun anInviteSender(
|
||||||
userId: UserId = UserId("@bob:domain"),
|
userId: UserId = UserId("@bob:domain"),
|
||||||
displayName: String = "Bob",
|
displayName: String = "Bob",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
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.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.toInviteData
|
import io.element.android.features.invite.api.toInviteData
|
||||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.root
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
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
|
||||||
|
data object ClearFailures : SpaceEvents
|
||||||
|
data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||||
|
data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||||
import io.element.android.libraries.androidutils.R
|
import io.element.android.libraries.androidutils.R
|
||||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||||
|
|
@ -36,11 +37,13 @@ class SpaceNode(
|
||||||
private val presenter: SpacePresenter,
|
private val presenter: SpacePresenter,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val spaceRoomList: SpaceRoomList,
|
private val spaceRoomList: SpaceRoomList,
|
||||||
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
||||||
fun onLeaveSpace()
|
fun onLeaveSpace()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins.filterIsInstance<Callback>().single()
|
private val callback = plugins.filterIsInstance<Callback>().single()
|
||||||
|
|
||||||
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
||||||
|
|
@ -76,6 +79,18 @@ class SpaceNode(
|
||||||
onShareSpace = {
|
onShareSpace = {
|
||||||
onShareRoom(context)
|
onShareRoom(context)
|
||||||
},
|
},
|
||||||
|
acceptDeclineInviteView = {
|
||||||
|
acceptDeclineInviteView.Render(
|
||||||
|
state = state.acceptDeclineInviteState,
|
||||||
|
onAcceptInviteSuccess = { roomId ->
|
||||||
|
callback.onOpenRoom(roomId, emptyList())
|
||||||
|
},
|
||||||
|
onDeclineInviteSuccess = { roomId ->
|
||||||
|
// No action needed
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,30 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.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.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.libraries.architecture.AsyncAction
|
||||||
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
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -33,6 +46,9 @@ class SpacePresenter(
|
||||||
private val spaceRoomList: SpaceRoomList,
|
private val spaceRoomList: SpaceRoomList,
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
private val joinRoom: JoinRoom,
|
||||||
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<SpaceState> {
|
) : Presenter<SpaceState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): SpaceState {
|
override fun present(): SpaceState {
|
||||||
|
|
@ -44,7 +60,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 ->
|
||||||
|
|
@ -56,10 +72,40 @@ class SpacePresenter(
|
||||||
}.collectAsState()
|
}.collectAsState()
|
||||||
|
|
||||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
||||||
|
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
|
||||||
|
|
||||||
|
LaunchedEffect(children) {
|
||||||
|
// 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) {
|
fun handleEvents(event: SpaceEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
SpaceEvents.LoadMore -> coroutineScope.paginate()
|
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
|
||||||
|
is SpaceEvents.Join -> {
|
||||||
|
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions)
|
||||||
|
}
|
||||||
|
SpaceEvents.ClearFailures -> {
|
||||||
|
val failedActions = joinActions
|
||||||
|
.filterValues { it is AsyncAction.Failure }
|
||||||
|
.mapValues { AsyncAction.Uninitialized }
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SpaceState(
|
return SpaceState(
|
||||||
|
|
@ -68,10 +114,27 @@ class SpacePresenter(
|
||||||
seenSpaceInvites = seenSpaceInvites,
|
seenSpaceInvites = seenSpaceInvites,
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
|
joinActions = joinActions.toPersistentMap(),
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.joinRoom(
|
||||||
|
spaceRoom: SpaceRoom,
|
||||||
|
joinActions: Map<RoomId, AsyncAction<Unit>>,
|
||||||
|
setJoinActions: (Map<RoomId, AsyncAction<Unit>>) -> Unit
|
||||||
|
) = launch {
|
||||||
|
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Loading))
|
||||||
|
joinRoom.invoke(
|
||||||
|
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
|
||||||
|
serverNames = spaceRoom.via,
|
||||||
|
trigger = JoinedRoom.Trigger.SpaceHierarchy,
|
||||||
|
).onFailure {
|
||||||
|
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.paginate() = launch {
|
private fun CoroutineScope.paginate() = launch {
|
||||||
spaceRoomList.paginate()
|
spaceRoomList.paginate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.root
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
data class SpaceState(
|
data class SpaceState(
|
||||||
|
|
@ -18,5 +21,12 @@ data class SpaceState(
|
||||||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||||
val hideInvitesAvatar: Boolean,
|
val hideInvitesAvatar: Boolean,
|
||||||
val hasMoreToLoad: Boolean,
|
val hasMoreToLoad: Boolean,
|
||||||
|
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
|
||||||
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val eventSink: (SpaceEvents) -> Unit
|
val eventSink: (SpaceEvents) -> Unit
|
||||||
)
|
) {
|
||||||
|
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
||||||
|
val hasAnyFailure: Boolean = joinActions.values.any {
|
||||||
|
it is AsyncAction.Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@
|
||||||
package io.element.android.features.space.impl.root
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
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
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
|
||||||
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
|
|
@ -33,8 +38,9 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
),
|
),
|
||||||
aSpaceState(
|
aSpaceState(
|
||||||
hasMoreToLoad = false,
|
hasMoreToLoad = false,
|
||||||
children = aListOfSpaceRooms()
|
children = aListOfSpaceRooms(),
|
||||||
),
|
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
||||||
|
)
|
||||||
// Add other states here
|
// Add other states here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -48,21 +54,36 @@ fun aSpaceState(
|
||||||
),
|
),
|
||||||
children: List<SpaceRoom> = emptyList(),
|
children: List<SpaceRoom> = emptyList(),
|
||||||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||||
|
joiningRooms: Set<RoomId> = emptySet(),
|
||||||
|
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
|
||||||
hideInvitesAvatar: Boolean = false,
|
hideInvitesAvatar: Boolean = false,
|
||||||
hasMoreToLoad: Boolean = false,
|
hasMoreToLoad: Boolean = false,
|
||||||
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
|
eventSink: (SpaceEvents) -> Unit = { },
|
||||||
) = SpaceState(
|
) = SpaceState(
|
||||||
currentSpace = parentSpace,
|
currentSpace = parentSpace,
|
||||||
children = children.toImmutableList(),
|
children = children.toImmutableList(),
|
||||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||||
hideInvitesAvatar = hideInvitesAvatar,
|
hideInvitesAvatar = hideInvitesAvatar,
|
||||||
hasMoreToLoad = hasMoreToLoad,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
eventSink = {}
|
joinActions = joinActions.toImmutableMap(),
|
||||||
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
||||||
return listOf(
|
return listOf(
|
||||||
aSpaceRoom(roomId = RoomId("!spaceId0:example.com")),
|
aSpaceRoom(
|
||||||
aSpaceRoom(roomId = RoomId("!spaceId1:example.com")),
|
roomId = RoomId("!spaceId0:example.com"),
|
||||||
aSpaceRoom(roomId = RoomId("!spaceId2:example.com")),
|
state = null,
|
||||||
|
),
|
||||||
|
aSpaceRoom(
|
||||||
|
roomId = RoomId("!spaceId1:example.com"),
|
||||||
|
state = CurrentUserMembership.JOINED,
|
||||||
|
),
|
||||||
|
aSpaceRoom(
|
||||||
|
roomId = RoomId("!spaceId2:example.com"),
|
||||||
|
state = CurrentUserMembership.INVITED,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -49,26 +53,29 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
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
|
||||||
|
import io.element.android.libraries.matrix.ui.components.JoinButton
|
||||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
||||||
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpaceView(
|
fun SpaceView(
|
||||||
state: SpaceState,
|
state: SpaceState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onLeaveSpaceClick: () -> Unit,
|
|
||||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
|
onLeaveSpaceClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
acceptDeclineInviteView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
SpaceViewTopBar(
|
SpaceViewTopBar(
|
||||||
state = state,
|
currentSpace = state.currentSpace,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
onShareSpace = onShareSpace,
|
onShareSpace = onShareSpace,
|
||||||
|
|
@ -82,11 +89,37 @@ fun SpaceView(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClick = onRoomClick
|
onRoomClick = onRoomClick
|
||||||
)
|
)
|
||||||
|
JoinRoomFailureEffect(
|
||||||
|
hasAnyFailure = state.hasAnyFailure,
|
||||||
|
eventSink = state.eventSink
|
||||||
|
)
|
||||||
|
acceptDeclineInviteView()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun JoinRoomFailureEffect(
|
||||||
|
hasAnyFailure: Boolean,
|
||||||
|
eventSink: (SpaceEvents) -> Unit,
|
||||||
|
) {
|
||||||
|
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||||
|
val updatedEventSink by rememberUpdatedState(eventSink)
|
||||||
|
AsyncIndicatorHost(modifier = Modifier, asyncIndicatorState)
|
||||||
|
LaunchedEffect(hasAnyFailure) {
|
||||||
|
if (hasAnyFailure) {
|
||||||
|
asyncIndicatorState.enqueue {
|
||||||
|
AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong))
|
||||||
|
}
|
||||||
|
delay(AsyncIndicator.DURATION_SHORT)
|
||||||
|
updatedEventSink(SpaceEvents.ClearFailures)
|
||||||
|
} else {
|
||||||
|
asyncIndicatorState.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpaceViewContent(
|
private fun SpaceViewContent(
|
||||||
state: SpaceState,
|
state: SpaceState,
|
||||||
|
|
@ -111,6 +144,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 = state.isJoining(spaceRoom.roomId)
|
||||||
SpaceRoomItemView(
|
SpaceRoomItemView(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||||
|
|
@ -120,7 +154,18 @@ private fun SpaceViewContent(
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
},
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,13 +200,12 @@ private fun LoadingMoreIndicator(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpaceViewTopBar(
|
private fun SpaceViewTopBar(
|
||||||
state: SpaceState,
|
currentSpace: SpaceRoom?,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val currentSpace = state.currentSpace
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|
@ -254,6 +298,40 @@ private fun SpaceAvatarAndNameRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SpaceRoom.trailingAction(
|
||||||
|
isCurrentlyJoining: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
): @Composable (() -> Unit)? {
|
||||||
|
return when (state) {
|
||||||
|
null, CurrentUserMembership.LEFT -> {
|
||||||
|
{
|
||||||
|
JoinButton(
|
||||||
|
showProgress = isCurrentlyJoining,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SpaceRoom.inviteButtons(
|
||||||
|
onAcceptClick: () -> Unit,
|
||||||
|
onDeclineClick: () -> Unit,
|
||||||
|
): @Composable (() -> Unit)? {
|
||||||
|
return when (state) {
|
||||||
|
CurrentUserMembership.INVITED -> {
|
||||||
|
@Composable {
|
||||||
|
InviteButtonsRowMolecule(
|
||||||
|
onAcceptClick = onAcceptClick,
|
||||||
|
onDeclineClick = onDeclineClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SpaceViewPreview(
|
internal fun SpaceViewPreview(
|
||||||
|
|
@ -261,9 +339,10 @@ internal fun SpaceViewPreview(
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
SpaceView(
|
SpaceView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = {},
|
|
||||||
onLeaveSpaceClick = {},
|
|
||||||
onRoomClick = {},
|
onRoomClick = {},
|
||||||
onShareSpace = {},
|
onShareSpace = {},
|
||||||
|
onLeaveSpaceClick = {},
|
||||||
|
acceptDeclineInviteView = {},
|
||||||
|
onBackClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,37 @@ package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
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.acceptdecline.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.toInviteData
|
||||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
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.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
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.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
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.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom
|
||||||
|
|
||||||
class SpacePresenterTest {
|
class SpacePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -39,6 +58,8 @@ class SpacePresenterTest {
|
||||||
assertThat(state.seenSpaceInvites).isEmpty()
|
assertThat(state.seenSpaceInvites).isEmpty()
|
||||||
assertThat(state.hideInvitesAvatar).isFalse()
|
assertThat(state.hideInvitesAvatar).isFalse()
|
||||||
assertThat(state.hasMoreToLoad).isTrue()
|
assertThat(state.hasMoreToLoad).isTrue()
|
||||||
|
assertThat(state.joinActions).isEmpty()
|
||||||
|
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
paginateResult.assertions().isCalledOnce()
|
paginateResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
@ -117,15 +138,184 @@ class SpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSpacePresenter(
|
@Test
|
||||||
|
fun `present - join a room success`() = runTest {
|
||||||
|
val joinRoom = lambdaRecorder<RoomIdOrAlias, List<String>, AnalyticsJoinedRoom.Trigger, Result<Unit>> { _, _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val serverNames = listOf("via1", "via2")
|
||||||
|
val aNotJoinedRoom = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
via = serverNames,
|
||||||
|
state = null,
|
||||||
|
)
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(
|
||||||
|
aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
state = CurrentUserMembership.JOINED,
|
||||||
|
),
|
||||||
|
aNotJoinedRoom,
|
||||||
|
),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
joinRoom = FakeJoinRoom(
|
||||||
|
lambda = joinRoom,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.joinActions[A_ROOM_ID_2]).isNull()
|
||||||
|
state.eventSink(SpaceEvents.Join(aNotJoinedRoom))
|
||||||
|
val joiningState = awaitItem()
|
||||||
|
assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading)
|
||||||
|
// Let the joinRoom call complete
|
||||||
|
advanceUntilIdle()
|
||||||
|
runCurrent()
|
||||||
|
// The room is joined
|
||||||
|
fakeSpaceRoomList.emitSpaceRooms(
|
||||||
|
listOf(
|
||||||
|
aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
state = CurrentUserMembership.JOINED,
|
||||||
|
),
|
||||||
|
aNotJoinedRoom.copy(state = CurrentUserMembership.JOINED),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(1)
|
||||||
|
val joinedState = awaitItem()
|
||||||
|
// Joined room is removed from the join actions
|
||||||
|
assertThat(joinedState.joinActions).doesNotContainKey(A_ROOM_ID_2)
|
||||||
|
joinRoom.assertions().isCalledOnce().with(
|
||||||
|
value(A_ROOM_ID_2.toRoomIdOrAlias()),
|
||||||
|
value(serverNames),
|
||||||
|
value(AnalyticsJoinedRoom.Trigger.SpaceHierarchy),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - join a room failure`() = runTest {
|
||||||
|
val aNotJoinedRoom = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
state = null,
|
||||||
|
)
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(
|
||||||
|
aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
state = CurrentUserMembership.JOINED,
|
||||||
|
),
|
||||||
|
aNotJoinedRoom,
|
||||||
|
),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
joinRoom = FakeJoinRoom(
|
||||||
|
lambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.joinActions[A_ROOM_ID_2]).isNull()
|
||||||
|
state.eventSink(SpaceEvents.Join(aNotJoinedRoom))
|
||||||
|
val joiningState = awaitItem()
|
||||||
|
assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading)
|
||||||
|
val errorState = awaitItem()
|
||||||
|
// Joined room is removed from the join actions
|
||||||
|
assertThat(errorState.joinActions[A_ROOM_ID_2]!!.isFailure()).isTrue()
|
||||||
|
// Clear error
|
||||||
|
errorState.eventSink(SpaceEvents.ClearFailures)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accept invite is transmitted to acceptDeclineInviteState`() {
|
||||||
|
`invite action is transmitted to acceptDeclineInviteState`(
|
||||||
|
acceptInvite = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - decline invite is transmitted to acceptDeclineInviteState`() {
|
||||||
|
`invite action is transmitted to acceptDeclineInviteState`(
|
||||||
|
acceptInvite = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun `invite action is transmitted to acceptDeclineInviteState`(
|
||||||
|
acceptInvite: Boolean,
|
||||||
|
) = runTest {
|
||||||
|
val eventRecorder = EventsRecorder<AcceptDeclineInviteEvents>()
|
||||||
|
val anInvitedRoom = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
state = CurrentUserMembership.INVITED,
|
||||||
|
)
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(
|
||||||
|
aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
state = CurrentUserMembership.JOINED,
|
||||||
|
),
|
||||||
|
anInvitedRoom,
|
||||||
|
),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
acceptDeclineInvitePresenter = {
|
||||||
|
anAcceptDeclineInviteState(
|
||||||
|
eventSink = eventRecorder,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.joinActions[A_ROOM_ID_2]).isNull()
|
||||||
|
if (acceptInvite) {
|
||||||
|
state.eventSink(SpaceEvents.AcceptInvite(anInvitedRoom))
|
||||||
|
eventRecorder.assertSingle(
|
||||||
|
AcceptDeclineInviteEvents.AcceptInvite(
|
||||||
|
invite = anInvitedRoom.toInviteData(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
state.eventSink(SpaceEvents.DeclineInvite(anInvitedRoom))
|
||||||
|
eventRecorder.assertSingle(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(
|
||||||
|
invite = anInvitedRoom.toInviteData(),
|
||||||
|
shouldConfirm = true,
|
||||||
|
blockUser = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createSpacePresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||||
|
joinRoom: JoinRoom = FakeJoinRoom(
|
||||||
|
lambda = { _, _, _ -> Result.success(Unit) },
|
||||||
|
),
|
||||||
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
): SpacePresenter {
|
): SpacePresenter {
|
||||||
return SpacePresenter(
|
return SpacePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
spaceRoomList = spaceRoomList,
|
spaceRoomList = spaceRoomList,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
|
joinRoom = joinRoom,
|
||||||
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SpaceStateTest {
|
||||||
|
@Test
|
||||||
|
fun `test default state`() {
|
||||||
|
val state = aSpaceState()
|
||||||
|
assertThat(state.hasAnyFailure).isFalse()
|
||||||
|
assertThat(state.isJoining(A_ROOM_ID)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test has failure`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
joinActions = mapOf(
|
||||||
|
A_ROOM_ID to AsyncAction.Uninitialized,
|
||||||
|
A_ROOM_ID_2 to AsyncAction.Failure(AN_EXCEPTION),
|
||||||
|
A_ROOM_ID_3 to AsyncAction.Success(Unit),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(state.hasAnyFailure).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test isJoining`() {
|
||||||
|
val state = aSpaceState(
|
||||||
|
joinActions = mapOf(
|
||||||
|
A_ROOM_ID to AsyncAction.Loading,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(state.isJoining(A_ROOM_ID)).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SpaceViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackClick = it,
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on a room name invokes the expected callback`() {
|
||||||
|
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, name = A_ROOM_NAME)
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnceWithParam(aSpaceRoom) {
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onRoomClick = it,
|
||||||
|
)
|
||||||
|
rule.onNodeWithText(A_ROOM_NAME).performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Join room emits the expected Event`() {
|
||||||
|
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null)
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_join)
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on accept invite emits the expected Event`() {
|
||||||
|
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on decline invite emits the expected Event`() {
|
||||||
|
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
|
||||||
|
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||||
|
rule.setSpaceView(
|
||||||
|
aSpaceState(
|
||||||
|
children = listOf(aSpaceRoom),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
||||||
|
state: SpaceState,
|
||||||
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
SpaceView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onRoomClick = onRoomClick,
|
||||||
|
onShareSpace = onShareSpace,
|
||||||
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
|
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies {
|
||||||
implementation(libs.coil.gif)
|
implementation(libs.coil.gif)
|
||||||
implementation(libs.coil.network.okhttp)
|
implementation(libs.coil.network.okhttp)
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
implementation(projects.libraries.previewutils)
|
||||||
|
|
||||||
testCommonDependencies(libs, true)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun JoinButton(
|
||||||
|
showProgress: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionAccent) {
|
||||||
|
TextButton(
|
||||||
|
modifier = modifier,
|
||||||
|
text = stringResource(CommonStrings.action_join),
|
||||||
|
onClick = onClick,
|
||||||
|
size = ButtonSize.LargeLowPadding,
|
||||||
|
showProgress = showProgress,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
|
@ -41,6 +42,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
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.unreadIndicator
|
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||||
|
|
@ -59,6 +62,8 @@ fun SpaceRoomItemView(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
trailingAction: @Composable (() -> Unit)? = null,
|
||||||
|
bottomAction: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
SpaceRoomItemScaffold(
|
SpaceRoomItemScaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -67,6 +72,7 @@ fun SpaceRoomItemView(
|
||||||
hideAvatars = hideAvatars,
|
hideAvatars = hideAvatars,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
trailingAction = trailingAction,
|
||||||
) {
|
) {
|
||||||
NameAndIndicatorRow(
|
NameAndIndicatorRow(
|
||||||
isSpace = spaceRoom.isSpace,
|
isSpace = spaceRoom.isSpace,
|
||||||
|
|
@ -79,22 +85,22 @@ fun SpaceRoomItemView(
|
||||||
subtitle = spaceRoom.subtitle()
|
subtitle = spaceRoom.subtitle()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(1.dp))
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
Text(
|
val info = spaceRoom.info()
|
||||||
modifier = Modifier.weight(1f),
|
if (info.isNotBlank()) {
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
Text(
|
||||||
text = spaceRoom.info(),
|
modifier = Modifier.weight(1f),
|
||||||
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
text = info,
|
||||||
maxLines = 1,
|
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
|
||||||
overflow = TextOverflow.Ellipsis
|
color = ElementTheme.colors.textSecondary,
|
||||||
)
|
maxLines = 1,
|
||||||
if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
overflow = TextOverflow.Ellipsis
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
InviteButtonsRowMolecule(
|
|
||||||
onAcceptClick = {},
|
|
||||||
onDeclineClick = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (bottomAction != null) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
bottomAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,7 +172,8 @@ private fun SpaceRoomItemScaffold(
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
hideAvatars: Boolean,
|
hideAvatars: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
trailingAction: @Composable (() -> Unit)? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val clickModifier = Modifier
|
val clickModifier = Modifier
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
|
|
@ -194,6 +201,10 @@ private fun SpaceRoomItemScaffold(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
if (trailingAction != null) {
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
trailingAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,3 +244,32 @@ private fun SpaceRoom.visibilityIcon(): ImageVector? {
|
||||||
CompoundIcons.LockSolid()
|
CompoundIcons.LockSolid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@PreviewsDayNight
|
||||||
|
internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview {
|
||||||
|
SpaceRoomItemView(
|
||||||
|
spaceRoom = spaceRoom,
|
||||||
|
showUnreadIndicator = spaceRoom.state == CurrentUserMembership.INVITED,
|
||||||
|
hideAvatars = false,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
||||||
|
{ InviteButtonsRowMolecule({}, {}) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
trailingAction = when (spaceRoom.state) {
|
||||||
|
null, CurrentUserMembership.LEFT -> {
|
||||||
|
{
|
||||||
|
JoinButton(
|
||||||
|
showProgress = spaceRoom.state == CurrentUserMembership.LEFT,
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
|
|
||||||
|
class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
|
||||||
|
override val values: Sequence<SpaceRoom> = sequenceOf(
|
||||||
|
aSpaceRoom(
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
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",
|
||||||
|
state = CurrentUserMembership.LEFT,
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
worldReadable = true,
|
||||||
|
roomId = RoomId("!spaceId0:example.com"),
|
||||||
|
),
|
||||||
|
aSpaceRoom(
|
||||||
|
numJoinedMembers = 5,
|
||||||
|
childrenCount = 10,
|
||||||
|
worldReadable = true,
|
||||||
|
avatarUrl = "anUrl",
|
||||||
|
roomId = RoomId("!spaceId1:example.com"),
|
||||||
|
state = CurrentUserMembership.LEFT,
|
||||||
|
),
|
||||||
|
aSpaceRoom(
|
||||||
|
name = null,
|
||||||
|
numJoinedMembers = 5,
|
||||||
|
childrenCount = 10,
|
||||||
|
worldReadable = true,
|
||||||
|
avatarUrl = "anUrl",
|
||||||
|
roomId = RoomId("!spaceId2:example.com"),
|
||||||
|
state = CurrentUserMembership.INVITED,
|
||||||
|
),
|
||||||
|
aSpaceRoom(
|
||||||
|
name = null,
|
||||||
|
numJoinedMembers = 5,
|
||||||
|
childrenCount = 10,
|
||||||
|
worldReadable = true,
|
||||||
|
avatarUrl = "anUrl",
|
||||||
|
roomId = RoomId("!spaceId3:example.com"),
|
||||||
|
state = CurrentUserMembership.INVITED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fbdebc1c9361339dd0db1051e59162ed62fa2787c46457848da5ee6a30474588
|
oid sha256:cc1c98131fdad16e22c63feb72536fee912ae84d763c57ff76eaee6d61889b4b
|
||||||
size 106570
|
size 127697
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7bb7d1c08f5b2551117aa1aff8a3afbabd0f4100e0fae11cf415bd8e851c0307
|
oid sha256:4ed12d6dc439b7e7d86580ee24a5cd5c3b5e86b13c4f3cf3e64f677632df5872
|
||||||
size 103835
|
size 124816
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a8eb29dd4b1667250d9bdf880bbf16fe343ac40704df793b1ba0725cbc036a56
|
oid sha256:2d5111d6ea6b79a4085b65c5d0390d8bbe8d200cc8e50bd9f3cd568ac9ab6179
|
||||||
size 46929
|
size 53740
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:48a538ac752a6f0a84d09bb6b81a7c7c8f47f01537d500de28040cb859db8498
|
oid sha256:377cf0bfc11eb067767b089cc5f35ff92307e3512eb30b6f9d51780acf590f9d
|
||||||
size 45652
|
size 52891
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0c95c84150a78322d6b101fe5ee4b9d0c8436ff9792ca87aa1d4394a0f86a345
|
oid sha256:75d18c001040426b961c5f7d0115a2dbad7e3cd1eb8a7af09357cdda1cc591a4
|
||||||
size 46364
|
size 52470
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5c88b9691b4dc811a5504dca5baa75f46dca6083b161ddf3c2e71050aa449d59
|
oid sha256:26e17ed257ded386bf0538c68f6848841f28d4ffd04a8d3f7d3b41feb203deb2
|
||||||
size 44862
|
size 51517
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6cb722a95b3ee29f751d053ea1cab634c3181b07678449600a2c23a97de0fcd1
|
||||||
|
size 16496
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:684f50c937b0ab7413cdff653a11ed11796583bf2182bfc1bbc1725f42d3a0ba
|
||||||
|
size 13073
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9f6b5eaa4a84fc70cab57d37737b390583d3af9f6e829cecffbcf36351bd40e2
|
||||||
|
size 23562
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0f271f630d93fdf49436509d08276b6a444db92c06fd06129af93cffbb4623d4
|
||||||
|
size 17931
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f9fed82716640b4c96b6ef666d9f992e966c17ebddc275c5f562b412e2d549b0
|
||||||
|
size 15065
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:79e7cecc4b03937d2a030721383664d54466e7ce25459c3628ace8b36b305214
|
||||||
|
size 35293
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7798eeef4a1de3312ec69b757963c6430d3952d45db6346961f79dae4e15913a
|
||||||
|
size 41389
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7798eeef4a1de3312ec69b757963c6430d3952d45db6346961f79dae4e15913a
|
||||||
|
size 41389
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f07736b16ece457794cdbd3859ede26ed0aa4b53acdc69d7f913df9a9b0fad1f
|
||||||
|
size 16002
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ad16996462532092686f1638f3ee1f40ede99b5ccb2bc53e57931f50920e62b5
|
||||||
|
size 12629
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:690df48431acad687f2a2e72aebba4fa63de433ed68b7b9e19818a78973cd211
|
||||||
|
size 22676
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:34073398379389aa862c892e6eaa33574606071597a0095dfa5c84b99154b5df
|
||||||
|
size 17218
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:13d555688a963f8bf664c8bafc397a48c31575d4a2c2fa06a20b191e620d23ea
|
||||||
|
size 14587
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3ac86fef06b52c5456c5eb828c1b60615792906f74d15ea35e3289d2cff47ee1
|
||||||
|
size 34183
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0b431eae85dcb3e5efde883a47c78aab6c346429366357bb5f6e740342eeeb97
|
||||||
|
size 40072
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0b431eae85dcb3e5efde883a47c78aab6c346429366357bb5f6e740342eeeb97
|
||||||
|
size 40072
|
||||||
Loading…
Add table
Add a link
Reference in a new issue