Merge pull request #5431 from element-hq/feature/fga/space_list_join_action
Feature : space list join action
This commit is contained in:
commit
83f59c2de3
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.search.RoomListSearchState
|
||||
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.anAcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
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.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -76,16 +74,6 @@ internal fun aLeaveRoomState(
|
|||
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> {
|
||||
return persistentListOf(
|
||||
aRoomListRoomSummary(
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ fun HomeSpacesView(
|
|||
},
|
||||
onLongClick = {
|
||||
// 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.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.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
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 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.ConfirmingDeclineInvite
|
||||
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||
import io.element.android.features.invite.impl.AcceptInvite
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
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 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.anAcceptDeclineInviteState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
|
|
@ -219,16 +219,6 @@ fun aJoinRoomState(
|
|||
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(
|
||||
userId: UserId = UserId("@bob:domain"),
|
||||
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.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.joinroom.impl.di.CancelKnockRoom
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
package io.element.android.features.space.impl.root
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
|
||||
sealed interface SpaceEvents {
|
||||
data object LoadMore : SpaceEvents
|
||||
data class Join(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||
data object ClearFailures : SpaceEvents
|
||||
data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||
data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.androidutils.R
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
|
|
@ -36,11 +37,13 @@ class SpaceNode(
|
|||
private val presenter: SpacePresenter,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
|
||||
fun onLeaveSpace()
|
||||
}
|
||||
|
||||
private val callback = plugins.filterIsInstance<Callback>().single()
|
||||
|
||||
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
||||
|
|
@ -76,6 +79,18 @@ class SpaceNode(
|
|||
onShareSpace = {
|
||||
onShareRoom(context)
|
||||
},
|
||||
acceptDeclineInviteView = {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = { roomId ->
|
||||
callback.onOpenRoom(roomId, emptyList())
|
||||
},
|
||||
onDeclineInviteSuccess = { roomId ->
|
||||
// No action needed
|
||||
},
|
||||
modifier = Modifier
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,30 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.Inject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.toInviteData
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
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
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -33,6 +46,9 @@ class SpacePresenter(
|
|||
private val spaceRoomList: SpaceRoomList,
|
||||
private val client: MatrixClient,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val joinRoom: JoinRoom,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
) : Presenter<SpaceState> {
|
||||
@Composable
|
||||
override fun present(): SpaceState {
|
||||
|
|
@ -44,7 +60,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 ->
|
||||
|
|
@ -56,10 +72,40 @@ class SpacePresenter(
|
|||
}.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) {
|
||||
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(
|
||||
|
|
@ -68,10 +114,27 @@ class SpacePresenter(
|
|||
seenSpaceInvites = seenSpaceInvites,
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
joinActions = joinActions.toPersistentMap(),
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
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 {
|
||||
spaceRoomList.paginate()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@
|
|||
|
||||
package io.element.android.features.space.impl.root
|
||||
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
data class SpaceState(
|
||||
|
|
@ -18,5 +21,12 @@ data class SpaceState(
|
|||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||
val hideInvitesAvatar: Boolean,
|
||||
val hasMoreToLoad: Boolean,
|
||||
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
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
|
||||
|
||||
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.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||
|
|
@ -33,8 +38,9 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
|||
),
|
||||
aSpaceState(
|
||||
hasMoreToLoad = false,
|
||||
children = aListOfSpaceRooms()
|
||||
),
|
||||
children = aListOfSpaceRooms(),
|
||||
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
||||
)
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
|
@ -48,21 +54,36 @@ fun aSpaceState(
|
|||
),
|
||||
children: List<SpaceRoom> = emptyList(),
|
||||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||
joiningRooms: Set<RoomId> = emptySet(),
|
||||
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
|
||||
hideInvitesAvatar: Boolean = false,
|
||||
hasMoreToLoad: Boolean = false,
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
eventSink: (SpaceEvents) -> Unit = { },
|
||||
) = SpaceState(
|
||||
currentSpace = parentSpace,
|
||||
children = children.toImmutableList(),
|
||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
eventSink = {}
|
||||
joinActions = joinActions.toImmutableMap(),
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
||||
return listOf(
|
||||
aSpaceRoom(roomId = RoomId("!spaceId0:example.com")),
|
||||
aSpaceRoom(roomId = RoomId("!spaceId1:example.com")),
|
||||
aSpaceRoom(roomId = RoomId("!spaceId2:example.com")),
|
||||
aSpaceRoom(
|
||||
roomId = RoomId("!spaceId0: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 io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||
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.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.matrix.api.room.CurrentUserMembership
|
||||
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.SpaceRoomItemView
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SpaceView(
|
||||
state: SpaceState,
|
||||
onBackClick: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||
onShareSpace: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
acceptDeclineInviteView: @Composable () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
SpaceViewTopBar(
|
||||
state = state,
|
||||
currentSpace = state.currentSpace,
|
||||
onBackClick = onBackClick,
|
||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||
onShareSpace = onShareSpace,
|
||||
|
|
@ -82,11 +89,37 @@ fun SpaceView(
|
|||
state = state,
|
||||
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
|
||||
private fun SpaceViewContent(
|
||||
state: SpaceState,
|
||||
|
|
@ -111,6 +144,7 @@ private fun SpaceViewContent(
|
|||
state.children.forEach { spaceRoom ->
|
||||
item {
|
||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||
SpaceRoomItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||
|
|
@ -120,7 +154,18 @@ private fun SpaceViewContent(
|
|||
},
|
||||
onLongClick = {
|
||||
// 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)
|
||||
@Composable
|
||||
private fun SpaceViewTopBar(
|
||||
state: SpaceState,
|
||||
currentSpace: SpaceRoom?,
|
||||
onBackClick: () -> Unit,
|
||||
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
||||
onShareSpace: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val currentSpace = state.currentSpace
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
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
|
||||
@Composable
|
||||
internal fun SpaceViewPreview(
|
||||
|
|
@ -261,9 +339,10 @@ internal fun SpaceViewPreview(
|
|||
) = ElementPreview {
|
||||
SpaceView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onLeaveSpaceClick = {},
|
||||
onRoomClick = {},
|
||||
onShareSpace = {},
|
||||
onLeaveSpaceClick = {},
|
||||
acceptDeclineInviteView = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,37 @@ package io.element.android.features.space.impl.root
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.acceptdecline.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.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.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.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.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
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.value
|
||||
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.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom
|
||||
|
||||
class SpacePresenterTest {
|
||||
@Test
|
||||
|
|
@ -39,6 +58,8 @@ class SpacePresenterTest {
|
|||
assertThat(state.seenSpaceInvites).isEmpty()
|
||||
assertThat(state.hideInvitesAvatar).isFalse()
|
||||
assertThat(state.hasMoreToLoad).isTrue()
|
||||
assertThat(state.joinActions).isEmpty()
|
||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||
advanceUntilIdle()
|
||||
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(),
|
||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
joinRoom: JoinRoom = FakeJoinRoom(
|
||||
lambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
): SpacePresenter {
|
||||
return SpacePresenter(
|
||||
client = client,
|
||||
spaceRoomList = spaceRoomList,
|
||||
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.network.okhttp)
|
||||
implementation(libs.jsoup)
|
||||
implementation(projects.libraries.previewutils)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
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.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
|
|
@ -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.AvatarType
|
||||
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.Text
|
||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||
|
|
@ -59,6 +62,8 @@ fun SpaceRoomItemView(
|
|||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
trailingAction: @Composable (() -> Unit)? = null,
|
||||
bottomAction: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
SpaceRoomItemScaffold(
|
||||
modifier = modifier,
|
||||
|
|
@ -67,6 +72,7 @@ fun SpaceRoomItemView(
|
|||
hideAvatars = hideAvatars,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
trailingAction = trailingAction,
|
||||
) {
|
||||
NameAndIndicatorRow(
|
||||
isSpace = spaceRoom.isSpace,
|
||||
|
|
@ -79,22 +85,22 @@ fun SpaceRoomItemView(
|
|||
subtitle = spaceRoom.subtitle()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = spaceRoom.info(),
|
||||
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
InviteButtonsRowMolecule(
|
||||
onAcceptClick = {},
|
||||
onDeclineClick = {},
|
||||
val info = spaceRoom.info()
|
||||
if (info.isNotBlank()) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = info,
|
||||
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (bottomAction != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
bottomAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +172,8 @@ private fun SpaceRoomItemScaffold(
|
|||
onLongClick: () -> Unit,
|
||||
hideAvatars: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
trailingAction: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val clickModifier = Modifier
|
||||
.combinedClickable(
|
||||
|
|
@ -194,6 +201,10 @@ private fun SpaceRoomItemScaffold(
|
|||
modifier = Modifier.weight(1f),
|
||||
content = content,
|
||||
)
|
||||
if (trailingAction != null) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
trailingAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,3 +244,32 @@ private fun SpaceRoom.visibilityIcon(): ImageVector? {
|
|||
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
|
||||
oid sha256:fbdebc1c9361339dd0db1051e59162ed62fa2787c46457848da5ee6a30474588
|
||||
size 106570
|
||||
oid sha256:cc1c98131fdad16e22c63feb72536fee912ae84d763c57ff76eaee6d61889b4b
|
||||
size 127697
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7bb7d1c08f5b2551117aa1aff8a3afbabd0f4100e0fae11cf415bd8e851c0307
|
||||
size 103835
|
||||
oid sha256:4ed12d6dc439b7e7d86580ee24a5cd5c3b5e86b13c4f3cf3e64f677632df5872
|
||||
size 124816
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a8eb29dd4b1667250d9bdf880bbf16fe343ac40704df793b1ba0725cbc036a56
|
||||
size 46929
|
||||
oid sha256:2d5111d6ea6b79a4085b65c5d0390d8bbe8d200cc8e50bd9f3cd568ac9ab6179
|
||||
size 53740
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:48a538ac752a6f0a84d09bb6b81a7c7c8f47f01537d500de28040cb859db8498
|
||||
size 45652
|
||||
oid sha256:377cf0bfc11eb067767b089cc5f35ff92307e3512eb30b6f9d51780acf590f9d
|
||||
size 52891
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0c95c84150a78322d6b101fe5ee4b9d0c8436ff9792ca87aa1d4394a0f86a345
|
||||
size 46364
|
||||
oid sha256:75d18c001040426b961c5f7d0115a2dbad7e3cd1eb8a7af09357cdda1cc591a4
|
||||
size 52470
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c88b9691b4dc811a5504dca5baa75f46dca6083b161ddf3c2e71050aa449d59
|
||||
size 44862
|
||||
oid sha256:26e17ed257ded386bf0538c68f6848841f28d4ffd04a8d3f7d3b41feb203deb2
|
||||
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