Merge pull request #5431 from element-hq/feature/fga/space_list_join_action
Feature : space list join action
This commit is contained in:
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user