From 7050076bebac46df01fcf15a8199cda306feb5c1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Sep 2025 16:52:56 +0200 Subject: [PATCH 01/13] feature (space) : add trailing action to SpaceRoomItemView --- .../features/space/impl/SpaceStateProvider.kt | 49 +++++++++------- libraries/matrixui/build.gradle.kts | 1 + .../matrix/ui/components/SpaceRoomItemView.kt | 39 ++++++++++--- .../matrix/ui/components/SpaceRoomProvider.kt | 56 +++++++++++++++++++ 4 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt index cf2fcf92b5..07ebaa0a90 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.space.impl 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.spaces.SpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom import kotlinx.collections.immutable.toImmutableList @@ -17,24 +18,22 @@ import kotlinx.collections.immutable.toImmutableSet open class SpaceStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aSpaceState(), aSpaceState( - parentSpace = aSpaceRoom( - name = null, - numJoinedMembers = 5, - childrenCount = 10, - worldReadable = true, - ), - hasMoreToLoad = true, + + ), aSpaceState( + parentSpace = aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, ), - aSpaceState( - hasMoreToLoad = true, - children = aListOfSpaceRooms(), - ), - aSpaceState( - hasMoreToLoad = false, - children = aListOfSpaceRooms() - ) + hasMoreToLoad = true, + ), aSpaceState( + hasMoreToLoad = true, + children = aListOfSpaceRooms(), + ), aSpaceState( + hasMoreToLoad = false, children = aListOfSpaceRooms() + ) // Add other states here ) } @@ -56,13 +55,21 @@ fun aSpaceState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - eventSink = {} -) + eventSink = {}) private fun aListOfSpaceRooms(): List { 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, + ), ) } diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index a500186587..96045f3a93 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -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) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index e599b75cab..c98c830c20 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -20,8 +20,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -31,6 +33,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,8 +44,12 @@ 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.ButtonSize import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.unreadIndicator import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.join.JoinRule @@ -59,6 +66,7 @@ fun SpaceRoomItemView( onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, + trailingAction: @Composable (() -> Unit)? = null, ) { SpaceRoomItemScaffold( modifier = modifier, @@ -67,16 +75,14 @@ fun SpaceRoomItemView( hideAvatars = hideAvatars, onClick = onClick, onLongClick = onLongClick, + trailingAction = trailingAction, ) { NameAndIndicatorRow( - isSpace = spaceRoom.isSpace, - name = spaceRoom.name, - showIndicator = showUnreadIndicator + isSpace = spaceRoom.isSpace, name = spaceRoom.name, showIndicator = showUnreadIndicator ) Spacer(modifier = Modifier.height(1.dp)) SubtitleRow( - visibilityIcon = spaceRoom.visibilityIcon(), - subtitle = spaceRoom.subtitle() + visibilityIcon = spaceRoom.visibilityIcon(), subtitle = spaceRoom.subtitle() ) Spacer(modifier = Modifier.height(1.dp)) Text( @@ -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( @@ -174,8 +181,7 @@ private fun SpaceRoomItemScaffold( onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), indication = ripple(), - interactionSource = remember { MutableInteractionSource() } - ) + interactionSource = remember { MutableInteractionSource() }) .onKeyboardContextMenuAction { onLongClick } Row( modifier = modifier @@ -194,6 +200,10 @@ private fun SpaceRoomItemScaffold( modifier = Modifier.weight(1f), content = content, ) + if (trailingAction != null) { + Spacer(modifier = Modifier.width(16.dp)) + trailingAction() + } } } @@ -233,3 +243,16 @@ 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() + ) +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt new file mode 100644 index 0000000000..c1c2700889 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt @@ -0,0 +1,56 @@ +/* + * 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 { + override val values: Sequence = sequenceOf( + aSpaceRoom( + roomType = RoomType.Room, + name = "Room name", + topic = "Room topic that is quite long and might be truncated" + ), + 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"), + ), + 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, + ), + ) +} From 8f0841673c34f5768b20e20be3f4a117205684b8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 Sep 2025 11:25:08 +0200 Subject: [PATCH 02/13] feature (space) : allow joining children from space screen --- .../home/impl/spaces/HomeSpacesView.kt | 2 +- .../features/space/impl/SpaceEvents.kt | 3 ++ .../features/space/impl/SpacePresenter.kt | 49 ++++++++++++++++--- .../android/features/space/impl/SpaceState.kt | 1 + .../features/space/impl/SpaceStateProvider.kt | 2 + .../android/features/space/impl/SpaceView.kt | 29 +++++++++++ .../space/impl/DefaultSpaceEntryPointTest.kt | 8 ++- .../features/space/impl/SpacePresenterTest.kt | 10 +++- 8 files changed, 95 insertions(+), 9 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt index 8b07b9f526..36e0bbc56a 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -64,7 +64,7 @@ fun HomeSpacesView( }, onLongClick = { // TODO - } + }, ) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt index 848dac3ebc..3314d5c39c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceEvents.kt @@ -7,6 +7,9 @@ package io.element.android.features.space.impl +import io.element.android.libraries.matrix.api.spaces.SpaceRoom + sealed interface SpaceEvents { data object LoadMore : SpaceEvents + data class Join(val spaceRoom: SpaceRoom): SpaceEvents } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt index f5bfdcc840..7ec8e4c226 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpacePresenter.kt @@ -9,18 +9,27 @@ package io.element.android.features.space.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.Inject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import kotlinx.collections.immutable.persistentSetOf @@ -31,14 +40,14 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull -@Inject -class SpacePresenter( +@Inject class SpacePresenter( @Assisted private val inputs: SpaceEntryPoint.Inputs, private val client: MatrixClient, private val seenInvitesStore: SeenInvitesStore, + private val joinRoom: JoinRoom, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { - @AssistedFactory - fun interface Factory { + @AssistedFactory fun interface Factory { fun create(inputs: SpaceEntryPoint.Inputs): SpacePresenter } @@ -54,7 +63,7 @@ class SpacePresenter( seenInvitesStore.seenRoomIds().map { it.toPersistentSet() } }.collectAsState(persistentSetOf()) - val coroutineScope = rememberCoroutineScope() + val localCoroutineScope = rememberCoroutineScope() val children by spaceRoomList.spaceRoomsFlow.collectAsState(emptyList()) val hasMoreToLoad by remember { spaceRoomList.paginationStatusFlow.mapState { status -> @@ -66,10 +75,24 @@ class SpacePresenter( }.collectAsState() val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() + val joiningRooms = remember { mutableStateOf(emptySet()) } + + LaunchedEffect(children, joiningRooms.value) { + val joinedChildren = children + .filter { it.state == CurrentUserMembership.JOINED } + .map { it.roomId } + .toSet() + joiningRooms.value.let { currentlyJoining -> + joiningRooms.value = currentlyJoining - joinedChildren + } + } fun handleEvents(event: SpaceEvents) { when (event) { - SpaceEvents.LoadMore -> coroutineScope.paginate() + SpaceEvents.LoadMore -> localCoroutineScope.paginate() + is SpaceEvents.Join -> { + sessionCoroutineScope.joinRoom(event.spaceRoom, joiningRooms) + } } } return SpaceState( @@ -78,10 +101,24 @@ class SpacePresenter( seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, + joiningRooms = joiningRooms.value.toPersistentSet(), eventSink = ::handleEvents, ) } + private fun CoroutineScope.joinRoom( + spaceRoom: SpaceRoom, joiningRooms: MutableState> + ) = launch { + joiningRooms.value = joiningRooms.value + spaceRoom.roomId + joinRoom.invoke( + roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(), + serverNames = spaceRoom.via, + trigger = JoinedRoom.Trigger.SpaceHierarchy, + ).onFailure { + joiningRooms.value = joiningRooms.value - spaceRoom.roomId + } + } + private fun CoroutineScope.paginate() = launch { spaceRoomList.paginate() } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt index ad822283ca..f3f7a2194e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceState.kt @@ -18,5 +18,6 @@ data class SpaceState( val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, val hasMoreToLoad: Boolean, + val joiningRooms: ImmutableSet, val eventSink: (SpaceEvents) -> Unit ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt index 07ebaa0a90..41a1e80f94 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceStateProvider.kt @@ -47,6 +47,7 @@ fun aSpaceState( ), children: List = emptyList(), seenSpaceInvites: Set = emptySet(), + joiningRooms: Set = emptySet(), hideInvitesAvatar: Boolean = false, hasMoreToLoad: Boolean = false, ) = SpaceState( @@ -55,6 +56,7 @@ fun aSpaceState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, + joiningRooms = joiningRooms.toImmutableSet(), eventSink = {}) private fun aListOfSpaceRooms(): List { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt index f1f8356701..7ca13504e0 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceView.kt @@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState @@ -35,9 +37,11 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom @@ -96,6 +100,7 @@ private fun SpaceViewContent( state.children.forEach { spaceRoom -> item { val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED + val isCurrentlyJoining = spaceRoom.roomId in state.joiningRooms SpaceRoomItemView( spaceRoom = spaceRoom, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, @@ -105,6 +110,9 @@ private fun SpaceViewContent( }, onLongClick = { // TODO + }, + trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) { + state.eventSink(SpaceEvents.Join(spaceRoom)) } ) } @@ -191,6 +199,27 @@ private fun SpaceAvatarAndNameRow( } } +private fun SpaceRoom.trailingAction( + isCurrentlyJoining: Boolean, + onClick: () -> Unit +): @Composable (() -> Unit)? { + return when (state) { + null, CurrentUserMembership.LEFT -> { + @Composable { + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionAccent) { + TextButton( + text = stringResource(CommonStrings.action_join), + onClick = onClick, + size = ButtonSize.LargeLowPadding, + showProgress = isCurrentlyJoining, + ) + } + } + } + else -> null + } +} + @PreviewsDayNight @Composable internal fun SpaceViewPreview( diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt index cafc825f6a..748533098e 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt @@ -15,10 +15,12 @@ import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -27,7 +29,7 @@ class DefaultSpaceEntryPointTest { val instantTaskExecutorRule = InstantTaskExecutorRule() @Test - fun `test node builder`() { + fun `test node builder`() = runTest { val entryPoint = DefaultSpaceEntryPoint() val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID) val parentNode = TestParentNode.create { buildContext, plugins -> @@ -44,6 +46,10 @@ class DefaultSpaceEntryPointTest { ) ), seenInvitesStore = InMemorySeenInvitesStore(), + joinRoom = FakeJoinRoom( + lambda = { _, _, _ -> Result.success(Unit) }, + ), + sessionCoroutineScope = backgroundScope, ) }, ) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt index 0bcd1303ae..461c23cb25 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/SpacePresenterTest.kt @@ -14,15 +14,18 @@ import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.libraries.previewutils.room.aSpaceRoom import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test @@ -153,15 +156,20 @@ class SpacePresenterTest { } } - private fun createSpacePresenter( + private fun TestScope.createSpacePresenter( inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID), client: MatrixClient = FakeMatrixClient(), seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), + joinRoom: JoinRoom = FakeJoinRoom( + lambda = { _, _, _ -> Result.success(Unit) }, + ), ): SpacePresenter { return SpacePresenter( inputs = inputs, client = client, seenInvitesStore = seenInvitesStore, + joinRoom = joinRoom, + sessionCoroutineScope = backgroundScope, ) } } From 526bc27a08781dccbec28dff3f83d2c0b289205c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Sep 2025 20:38:55 +0200 Subject: [PATCH 03/13] feature (space) : manage failures to join in Space screen --- .../features/space/impl/root/SpaceEvents.kt | 1 + .../space/impl/root/SpacePresenter.kt | 36 ++++++++------- .../features/space/impl/root/SpaceState.kt | 11 ++++- .../space/impl/root/SpaceStateProvider.kt | 6 ++- .../features/space/impl/root/SpaceView.kt | 44 +++++++++++++++---- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index 97bffcb7d2..ece17889a0 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -12,4 +12,5 @@ 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 } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index b322501c25..c709e9e5f3 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -17,10 +17,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory -import im.vector.app.features.analytics.plan.JoinedRoom import dev.zacsweers.metro.AssistedInject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.di.annotations.SessionCoroutineScope @@ -34,14 +35,14 @@ 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 import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull -@AssistedInject -class SpacePresenter( +@AssistedInject class SpacePresenter( @Assisted private val inputs: SpaceEntryPoint.Inputs, private val client: MatrixClient, private val seenInvitesStore: SeenInvitesStore, @@ -76,15 +77,12 @@ class SpacePresenter( }.collectAsState() val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() - val joiningRooms = remember { mutableStateOf(emptySet()) } + val joinActions = remember { mutableStateOf(emptyMap>()) } - LaunchedEffect(children, joiningRooms.value) { - val joinedChildren = children - .filter { it.state == CurrentUserMembership.JOINED } - .map { it.roomId } - .toSet() - joiningRooms.value.let { currentlyJoining -> - joiningRooms.value = currentlyJoining - joinedChildren + LaunchedEffect(children) { + val joinedChildren = children.filter { it.state == CurrentUserMembership.JOINED }.map { it.roomId }.toSet() + joinActions.value.let { currentlyJoining -> + joinActions.value = currentlyJoining - joinedChildren } } @@ -92,7 +90,13 @@ class SpacePresenter( when (event) { SpaceEvents.LoadMore -> localCoroutineScope.paginate() is SpaceEvents.Join -> { - sessionCoroutineScope.joinRoom(event.spaceRoom, joiningRooms) + sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions) + } + SpaceEvents.ClearFailures -> { + val failedActions = joinActions.value + .filterValues { it is AsyncAction.Failure } + .mapValues { AsyncAction.Uninitialized } + joinActions.value = joinActions.value + failedActions } } } @@ -102,21 +106,21 @@ class SpacePresenter( seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joiningRooms = joiningRooms.value.toPersistentSet(), + joinActions = joinActions.value.toPersistentMap(), eventSink = ::handleEvents, ) } private fun CoroutineScope.joinRoom( - spaceRoom: SpaceRoom, joiningRooms: MutableState> + spaceRoom: SpaceRoom, joiningRooms: MutableState>> ) = launch { - joiningRooms.value = joiningRooms.value + spaceRoom.roomId + joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Loading) joinRoom.invoke( roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(), serverNames = spaceRoom.via, trigger = JoinedRoom.Trigger.SpaceHierarchy, ).onFailure { - joiningRooms.value = joiningRooms.value - spaceRoom.roomId + joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index acba9ab03f..5a629f94dd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -7,9 +7,11 @@ package io.element.android.features.space.impl.root +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,6 +20,11 @@ data class SpaceState( val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, val hasMoreToLoad: Boolean, - val joiningRooms: ImmutableSet, + val joinActions: ImmutableMap>, val eventSink: (SpaceEvents) -> Unit -) +) { + fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading + val hasAnyFailure: Boolean = joinActions.values.any { + it is AsyncAction.Failure + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 41a1e80f94..ac2327c8a7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -5,14 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.space.impl +package io.element.android.features.space.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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 { @@ -56,7 +58,7 @@ fun aSpaceState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joiningRooms = joiningRooms.toImmutableSet(), + joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(), eventSink = {}) private fun aListOfSpaceRooms(): List { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 860f73f318..c8000d84d6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor @@ -34,6 +35,9 @@ 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.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 @@ -58,22 +62,22 @@ 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, ) { Scaffold( modifier = modifier, topBar = { SpaceViewTopBar( - state = state, - onBackClick = onBackClick, + currentSpace = state.currentSpace, onBackClick = onBackClick, onLeaveSpaceClick = onLeaveSpaceClick, onShareSpace = onShareSpace, ) @@ -89,6 +93,30 @@ fun SpaceView( } }, ) + JoinRoomFailureEffect( + hasAnyFailure = state.hasAnyFailure, + eventSink = state.eventSink + ) +} + +@Composable +private fun JoinRoomFailureEffect( + hasAnyFailure: Boolean, + eventSink: (SpaceEvents) -> Unit, +) { + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState) + LaunchedEffect(hasAnyFailure) { + if (hasAnyFailure) { + asyncIndicatorState.enqueue { + AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong)) + } + delay(AsyncIndicator.DURATION_SHORT) + eventSink(SpaceEvents.ClearFailures) + } else { + asyncIndicatorState.clear() + } + } } @Composable @@ -115,7 +143,7 @@ private fun SpaceViewContent( state.children.forEach { spaceRoom -> item { val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED - val isCurrentlyJoining = spaceRoom.roomId in state.joiningRooms + val isCurrentlyJoining = state.isJoining(spaceRoom.roomId) SpaceRoomItemView( spaceRoom = spaceRoom, showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites, @@ -163,13 +191,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 = { @@ -229,6 +256,7 @@ private fun SpaceViewTopBar( ) */ } + }, ) } @@ -290,9 +318,9 @@ internal fun SpaceViewPreview( ) = ElementPreview { SpaceView( state = state, - onBackClick = {}, - onLeaveSpaceClick = {}, onRoomClick = {}, onShareSpace = {}, + onLeaveSpaceClick = {}, + onBackClick = {}, ) } From 183fad675ef70d8a0d9504b2085f489edcdea7d3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Sep 2025 20:41:52 +0200 Subject: [PATCH 04/13] feature (space) : fix breaking tests after rebase --- .../space/impl/DefaultSpaceEntryPointTest.kt | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt index 748533098e..0410944dbb 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt @@ -9,18 +9,13 @@ package io.element.android.features.space.impl import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat -import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom -import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList -import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode -import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -28,42 +23,27 @@ class DefaultSpaceEntryPointTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Test - fun `test node builder`() = runTest { + fun `test node builder`() { val entryPoint = DefaultSpaceEntryPoint() val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID) val parentNode = TestParentNode.create { buildContext, plugins -> - SpaceNode( + SpaceFlowNode( buildContext = buildContext, plugins = plugins, - presenterFactory = { inputs -> - assertThat(inputs).isEqualTo(nodeInputs) - SpacePresenter( - inputs = inputs, - client = FakeMatrixClient( - spaceService = FakeSpaceService( - spaceRoomListResult = { FakeSpaceRoomList() }, - ) - ), - seenInvitesStore = InMemorySeenInvitesStore(), - joinRoom = FakeJoinRoom( - lambda = { _, _, _ -> Result.success(Unit) }, - ), - sessionCoroutineScope = backgroundScope, - ) - }, ) } val callback = object : SpaceEntryPoint.Callback { - override fun onOpenRoom(roomId: RoomId, viaParameters: List) { - lambdaError() - } + override fun onOpenRoom(roomId: RoomId, viaParameters: List) = lambdaError() } val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) .inputs(nodeInputs) .callback(callback) .build() - assertThat(result).isInstanceOf(SpaceNode::class.java) + assertThat(result).isInstanceOf(SpaceFlowNode::class.java) assertThat(result.plugins).contains(nodeInputs) assertThat(result.plugins).contains(callback) } From de4e3d8735870b1a4d4b5af008d51179d7d18fc6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Sep 2025 21:27:41 +0200 Subject: [PATCH 05/13] feature (space) : some code clean up --- .../features/space/impl/root/SpaceEvents.kt | 2 +- .../space/impl/root/SpaceStateProvider.kt | 15 +++++++++------ .../features/space/impl/root/SpaceView.kt | 18 +++++++++--------- .../matrix/ui/components/SpaceRoomItemView.kt | 14 +++++++------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index ece17889a0..90e111fc88 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -11,6 +11,6 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom sealed interface SpaceEvents { data object LoadMore : SpaceEvents - data class Join(val spaceRoom: SpaceRoom): SpaceEvents + data class Join(val spaceRoom: SpaceRoom) : SpaceEvents data object ClearFailures : SpaceEvents } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index ac2327c8a7..1f5feb1850 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -20,9 +20,8 @@ import kotlinx.collections.immutable.toImmutableSet open class SpaceStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( + aSpaceState(), aSpaceState( - - ), aSpaceState( parentSpace = aSpaceRoom( name = null, numJoinedMembers = 5, @@ -30,11 +29,14 @@ open class SpaceStateProvider : PreviewParameterProvider { worldReadable = true, ), hasMoreToLoad = true, - ), aSpaceState( + ), + aSpaceState( hasMoreToLoad = true, children = aListOfSpaceRooms(), - ), aSpaceState( - hasMoreToLoad = false, children = aListOfSpaceRooms() + ), + aSpaceState( + hasMoreToLoad = false, + children = aListOfSpaceRooms() ) // Add other states here ) @@ -59,7 +61,8 @@ fun aSpaceState( hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(), - eventSink = {}) + eventSink = {} +) private fun aListOfSpaceRooms(): List { return listOf( diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index c8000d84d6..87daa6a1a8 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor @@ -77,7 +76,8 @@ fun SpaceView( modifier = modifier, topBar = { SpaceViewTopBar( - currentSpace = state.currentSpace, onBackClick = onBackClick, + currentSpace = state.currentSpace, + onBackClick = onBackClick, onLeaveSpaceClick = onLeaveSpaceClick, onShareSpace = onShareSpace, ) @@ -90,13 +90,13 @@ fun SpaceView( state = state, onRoomClick = onRoomClick ) + JoinRoomFailureEffect( + hasAnyFailure = state.hasAnyFailure, + eventSink = state.eventSink + ) } }, ) - JoinRoomFailureEffect( - hasAnyFailure = state.hasAnyFailure, - eventSink = state.eventSink - ) } @Composable @@ -105,14 +105,15 @@ private fun JoinRoomFailureEffect( eventSink: (SpaceEvents) -> Unit, ) { val asyncIndicatorState = rememberAsyncIndicatorState() - AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState) + 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) - eventSink(SpaceEvents.ClearFailures) + updatedEventSink(SpaceEvents.ClearFailures) } else { asyncIndicatorState.clear() } @@ -256,7 +257,6 @@ private fun SpaceViewTopBar( ) */ } - }, ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index c98c830c20..502b1b808c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -20,10 +20,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.ripple import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -46,10 +44,8 @@ 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.ButtonSize import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.unreadIndicator import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.join.JoinRule @@ -78,11 +74,14 @@ fun SpaceRoomItemView( trailingAction = trailingAction, ) { NameAndIndicatorRow( - isSpace = spaceRoom.isSpace, name = spaceRoom.name, showIndicator = showUnreadIndicator + isSpace = spaceRoom.isSpace, + name = spaceRoom.name, + showIndicator = showUnreadIndicator ) Spacer(modifier = Modifier.height(1.dp)) SubtitleRow( - visibilityIcon = spaceRoom.visibilityIcon(), subtitle = spaceRoom.subtitle() + visibilityIcon = spaceRoom.visibilityIcon(), + subtitle = spaceRoom.subtitle() ) Spacer(modifier = Modifier.height(1.dp)) Text( @@ -181,7 +180,8 @@ private fun SpaceRoomItemScaffold( onLongClick = onLongClick, onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), indication = ripple(), - interactionSource = remember { MutableInteractionSource() }) + interactionSource = remember { MutableInteractionSource() } + ) .onKeyboardContextMenuAction { onLongClick } Row( modifier = modifier From dbffad29d0597b6309f53c5fde5e6bdb4ebb7348 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 30 Sep 2025 15:59:29 +0200 Subject: [PATCH 06/13] feature (space) : handle accept decline invite --- .../features/space/impl/root/SpaceEvents.kt | 2 + .../features/space/impl/root/SpaceNode.kt | 14 ++++++ .../space/impl/root/SpacePresenter.kt | 45 +++++++++++++------ .../features/space/impl/root/SpaceState.kt | 2 + .../space/impl/root/SpaceStateProvider.kt | 14 ++++++ .../features/space/impl/root/SpaceView.kt | 31 ++++++++++++- .../space/impl/root/SpacePresenterTest.kt | 4 ++ .../matrix/ui/components/SpaceRoomItemView.kt | 36 ++++++++------- .../matrix/ui/components/SpaceRoomProvider.kt | 17 ++++++- 9 files changed, 135 insertions(+), 30 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index 90e111fc88..ab94ef719b 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -13,4 +13,6 @@ 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 } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt index 768bd9c795..5c2cae4128 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -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.api.SpaceEntryPoint import io.element.android.libraries.androidutils.R import io.element.android.libraries.androidutils.system.startSharePlainTextIntent @@ -36,6 +37,7 @@ class SpaceNode( @Assisted plugins: List, presenterFactory: SpacePresenter.Factory, private val matrixClient: MatrixClient, + private val acceptDeclineInviteView: AcceptDeclineInviteView, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun onOpenRoom(roomId: RoomId, viaParameters: List) @@ -79,6 +81,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 ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index c709e9e5f3..1909926716 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -9,7 +9,6 @@ package io.element.android.features.space.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -20,6 +19,9 @@ import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject 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.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter @@ -47,6 +49,7 @@ import kotlin.jvm.optionals.getOrNull private val client: MatrixClient, private val seenInvitesStore: SeenInvitesStore, private val joinRoom: JoinRoom, + private val acceptDeclineInvitePresenter: Presenter, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory fun interface Factory { @@ -77,26 +80,39 @@ import kotlin.jvm.optionals.getOrNull }.collectAsState() val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() - val joinActions = remember { mutableStateOf(emptyMap>()) } + val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } LaunchedEffect(children) { - val joinedChildren = children.filter { it.state == CurrentUserMembership.JOINED }.map { it.roomId }.toSet() - joinActions.value.let { currentlyJoining -> - joinActions.value = currentlyJoining - joinedChildren - } + // 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 -> localCoroutineScope.paginate() is SpaceEvents.Join -> { - sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions) + sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions) } SpaceEvents.ClearFailures -> { - val failedActions = joinActions.value + val failedActions = joinActions .filterValues { it is AsyncAction.Failure } .mapValues { AsyncAction.Uninitialized } - joinActions.value = joinActions.value + failedActions + 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) + ) } } } @@ -106,21 +122,24 @@ import kotlin.jvm.optionals.getOrNull seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joinActions = joinActions.value.toPersistentMap(), + joinActions = joinActions.toPersistentMap(), + acceptDeclineInviteState = acceptDeclineInviteState, eventSink = ::handleEvents, ) } private fun CoroutineScope.joinRoom( - spaceRoom: SpaceRoom, joiningRooms: MutableState>> + spaceRoom: SpaceRoom, + joinActions: Map>, + setJoinActions: (Map>) -> Unit ) = launch { - joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Loading) + setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Loading)) joinRoom.invoke( roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(), serverNames = spaceRoom.via, trigger = JoinedRoom.Trigger.SpaceHierarchy, ).onFailure { - joiningRooms.value = joiningRooms.value + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)) + setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it))) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index 5a629f94dd..ed6bc3dcf7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -7,6 +7,7 @@ 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 @@ -21,6 +22,7 @@ data class SpaceState( val hideInvitesAvatar: Boolean, val hasMoreToLoad: Boolean, val joinActions: ImmutableMap>, + val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (SpaceEvents) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 1f5feb1850..7b91da640f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -8,6 +8,8 @@ package io.element.android.features.space.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents +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.room.CurrentUserMembership @@ -54,6 +56,7 @@ fun aSpaceState( joiningRooms: Set = emptySet(), hideInvitesAvatar: Boolean = false, hasMoreToLoad: Boolean = false, + acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), ) = SpaceState( currentSpace = parentSpace, children = children.toImmutableList(), @@ -61,9 +64,20 @@ fun aSpaceState( hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(), + acceptDeclineInviteState = acceptDeclineInviteState, eventSink = {} ) +internal fun anAcceptDeclineInviteState( + acceptAction: AsyncAction = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {} +) = AcceptDeclineInviteState( + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) + private fun aListOfSpaceRooms(): List { return listOf( aSpaceRoom( diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 87daa6a1a8..d3cbe7bb44 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -34,6 +34,7 @@ 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 @@ -71,6 +72,7 @@ fun SpaceView( onShareSpace: () -> Unit, onLeaveSpaceClick: () -> Unit, modifier: Modifier = Modifier, + acceptDeclineInviteView: @Composable () -> Unit, ) { Scaffold( modifier = modifier, @@ -94,6 +96,7 @@ fun SpaceView( hasAnyFailure = state.hasAnyFailure, eventSink = state.eventSink ) + acceptDeclineInviteView() } }, ) @@ -157,7 +160,15 @@ private fun SpaceViewContent( }, 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)) + } + ) ) } } @@ -311,6 +322,23 @@ private fun SpaceRoom.trailingAction( } } +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( @@ -321,6 +349,7 @@ internal fun SpaceViewPreview( onRoomClick = {}, onShareSpace = {}, onLeaveSpaceClick = {}, + acceptDeclineInviteView = {}, onBackClick = {}, ) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 0204a05f0b..5845527223 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -11,8 +11,10 @@ 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.AcceptDeclineInviteState import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList @@ -163,12 +165,14 @@ class SpacePresenterTest { joinRoom: JoinRoom = FakeJoinRoom( lambda = { _, _, _ -> Result.success(Unit) }, ), + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, ): SpacePresenter { return SpacePresenter( inputs = inputs, client = client, seenInvitesStore = seenInvitesStore, joinRoom = joinRoom, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, sessionCoroutineScope = backgroundScope, ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index 502b1b808c..7f4cab1baa 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -63,6 +63,7 @@ fun SpaceRoomItemView( onLongClick: () -> Unit, modifier: Modifier = Modifier, trailingAction: @Composable (() -> Unit)? = null, + bottomAction: @Composable (() -> Unit)? = null, ) { SpaceRoomItemScaffold( modifier = modifier, @@ -84,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() + } } } @@ -253,6 +254,11 @@ internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class hideAvatars = false, onClick = {}, onLongClick = {}, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) { + { InviteButtonsRowMolecule({}, {}) } + } else { + null + } ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt index c1c2700889..38ffbfe2d3 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt @@ -18,9 +18,24 @@ class SpaceRoomProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( aSpaceRoom( roomType = RoomType.Room, - name = "Room name", + 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", + ), + 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, From 330f67554168135f3c7aec946dcff3b7e286aeff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Oct 2025 10:35:36 +0200 Subject: [PATCH 07/13] Improve Previews. --- .../space/impl/root/SpaceStateProvider.kt | 29 +++++++++-------- .../features/space/impl/root/SpaceView.kt | 19 ++++------- .../matrix/ui/components/JoinButton.kt | 32 +++++++++++++++++++ .../matrix/ui/components/SpaceRoomItemView.kt | 11 +++++++ .../matrix/ui/components/SpaceRoomProvider.kt | 2 ++ 5 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 7b91da640f..467ec6ae2b 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -24,22 +24,23 @@ open class SpaceStateProvider : PreviewParameterProvider { get() = sequenceOf( aSpaceState(), aSpaceState( - parentSpace = aSpaceRoom( - name = null, - numJoinedMembers = 5, - childrenCount = 10, - worldReadable = true, + parentSpace = aSpaceRoom( + name = null, + numJoinedMembers = 5, + childrenCount = 10, + worldReadable = true, + ), + hasMoreToLoad = true, ), - hasMoreToLoad = true, - ), aSpaceState( - hasMoreToLoad = true, - children = aListOfSpaceRooms(), - ), + hasMoreToLoad = true, + children = aListOfSpaceRooms(), + ), aSpaceState( - hasMoreToLoad = false, - children = aListOfSpaceRooms() - ) + hasMoreToLoad = false, + children = aListOfSpaceRooms(), + joiningRooms = setOf(RoomId("!spaceId0:example.com")), + ) // Add other states here ) } @@ -63,7 +64,7 @@ fun aSpaceState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joinActions = joiningRooms.associateWith { AsyncAction.Uninitialized }.toImmutableMap(), + joinActions = joiningRooms.associateWith { AsyncAction.Loading }.toImmutableMap(), acceptDeclineInviteState = acceptDeclineInviteState, eventSink = {} ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index d3cbe7bb44..870e294ba5 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -14,9 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -45,7 +43,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem @@ -53,10 +50,10 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom +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 @@ -307,15 +304,11 @@ private fun SpaceRoom.trailingAction( ): @Composable (() -> Unit)? { return when (state) { null, CurrentUserMembership.LEFT -> { - @Composable { - CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionAccent) { - TextButton( - text = stringResource(CommonStrings.action_join), - onClick = onClick, - size = ButtonSize.LargeLowPadding, - showProgress = isCurrentlyJoining, - ) - } + { + JoinButton( + showProgress = isCurrentlyJoining, + onClick = onClick, + ) } } else -> null diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt new file mode 100644 index 0000000000..1de19570f9 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt @@ -0,0 +1,32 @@ +/* + * 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.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, +) { + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionAccent) { + TextButton( + text = stringResource(CommonStrings.action_join), + onClick = onClick, + size = ButtonSize.LargeLowPadding, + showProgress = showProgress, + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index 7f4cab1baa..1abf0ad95e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -259,6 +259,17 @@ internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class { InviteButtonsRowMolecule({}, {}) } } else { null + }, + trailingAction = when (spaceRoom.state) { + null, CurrentUserMembership.LEFT -> { + { + JoinButton( + showProgress = spaceRoom.state == CurrentUserMembership.LEFT, + onClick = { }, + ) + } + } + else -> null } ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt index 38ffbfe2d3..bfdfeeac59 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt @@ -24,6 +24,7 @@ class SpaceRoomProvider : PreviewParameterProvider { aSpaceRoom( roomType = RoomType.Room, name = "Room name no topic", + state = CurrentUserMembership.LEFT, ), aSpaceRoom( roomType = RoomType.Room, @@ -48,6 +49,7 @@ class SpaceRoomProvider : PreviewParameterProvider { worldReadable = true, avatarUrl = "anUrl", roomId = RoomId("!spaceId1:example.com"), + state = CurrentUserMembership.LEFT, ), aSpaceRoom( name = null, From d449294f44ae5341cbb4a2b61d346ced1f5ab179 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 1 Oct 2025 09:04:00 +0000 Subject: [PATCH 08/13] Update screenshots --- .../features.home.impl.spaces_HomeSpacesView_Day_0_en.png | 4 ++-- .../features.home.impl.spaces_HomeSpacesView_Night_0_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_2_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Day_3_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_2_en.png | 4 ++-- .../images/features.space.impl.root_SpaceView_Night_3_en.png | 4 ++-- ...raries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png | 3 +++ ...raries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png | 3 +++ ...ries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png | 3 +++ 22 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png index df4bd9bbc2..e574e4911e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbdebc1c9361339dd0db1051e59162ed62fa2787c46457848da5ee6a30474588 -size 106570 +oid sha256:cc1c98131fdad16e22c63feb72536fee912ae84d763c57ff76eaee6d61889b4b +size 127697 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png index 66babf5423..41a0545535 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bb7d1c08f5b2551117aa1aff8a3afbabd0f4100e0fae11cf415bd8e851c0307 -size 103835 +oid sha256:4ed12d6dc439b7e7d86580ee24a5cd5c3b5e86b13c4f3cf3e64f677632df5872 +size 124816 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png index a3cdce72f3..3b2e097630 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8eb29dd4b1667250d9bdf880bbf16fe343ac40704df793b1ba0725cbc036a56 -size 46929 +oid sha256:2d5111d6ea6b79a4085b65c5d0390d8bbe8d200cc8e50bd9f3cd568ac9ab6179 +size 53740 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png index 9a52e1513f..7b987b6d2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48a538ac752a6f0a84d09bb6b81a7c7c8f47f01537d500de28040cb859db8498 -size 45652 +oid sha256:377cf0bfc11eb067767b089cc5f35ff92307e3512eb30b6f9d51780acf590f9d +size 52891 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png index 7e59d160b0..f018c0a4a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c95c84150a78322d6b101fe5ee4b9d0c8436ff9792ca87aa1d4394a0f86a345 -size 46364 +oid sha256:75d18c001040426b961c5f7d0115a2dbad7e3cd1eb8a7af09357cdda1cc591a4 +size 52470 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png index 566cd3ac21..dab60e5012 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c88b9691b4dc811a5504dca5baa75f46dca6083b161ddf3c2e71050aa449d59 -size 44862 +oid sha256:26e17ed257ded386bf0538c68f6848841f28d4ffd04a8d3f7d3b41feb203deb2 +size 51517 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png new file mode 100644 index 0000000000..53e8c50c72 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cb722a95b3ee29f751d053ea1cab634c3181b07678449600a2c23a97de0fcd1 +size 16496 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png new file mode 100644 index 0000000000..2268d59ba5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:684f50c937b0ab7413cdff653a11ed11796583bf2182bfc1bbc1725f42d3a0ba +size 13073 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png new file mode 100644 index 0000000000..b90c8c5ba1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f6b5eaa4a84fc70cab57d37737b390583d3af9f6e829cecffbcf36351bd40e2 +size 23562 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png new file mode 100644 index 0000000000..eeafaa0dab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f271f630d93fdf49436509d08276b6a444db92c06fd06129af93cffbb4623d4 +size 17931 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png new file mode 100644 index 0000000000..f925a8d461 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fed82716640b4c96b6ef666d9f992e966c17ebddc275c5f562b412e2d549b0 +size 15065 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png new file mode 100644 index 0000000000..8ea41b3e64 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79e7cecc4b03937d2a030721383664d54466e7ce25459c3628ace8b36b305214 +size 35293 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png new file mode 100644 index 0000000000..7b1b324268 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7798eeef4a1de3312ec69b757963c6430d3952d45db6346961f79dae4e15913a +size 41389 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png new file mode 100644 index 0000000000..7b1b324268 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7798eeef4a1de3312ec69b757963c6430d3952d45db6346961f79dae4e15913a +size 41389 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png new file mode 100644 index 0000000000..3663e98d52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f07736b16ece457794cdbd3859ede26ed0aa4b53acdc69d7f913df9a9b0fad1f +size 16002 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png new file mode 100644 index 0000000000..244e0ce786 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad16996462532092686f1638f3ee1f40ede99b5ccb2bc53e57931f50920e62b5 +size 12629 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png new file mode 100644 index 0000000000..5085864cc6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:690df48431acad687f2a2e72aebba4fa63de433ed68b7b9e19818a78973cd211 +size 22676 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png new file mode 100644 index 0000000000..1e85e954f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34073398379389aa862c892e6eaa33574606071597a0095dfa5c84b99154b5df +size 17218 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png new file mode 100644 index 0000000000..45f9f23c79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13d555688a963f8bf664c8bafc397a48c31575d4a2c2fa06a20b191e620d23ea +size 14587 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png new file mode 100644 index 0000000000..4c0e5e6671 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac86fef06b52c5456c5eb828c1b60615792906f74d15ea35e3289d2cff47ee1 +size 34183 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png new file mode 100644 index 0000000000..d2f9e318e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b431eae85dcb3e5efde883a47c78aab6c346429366357bb5f6e740342eeeb97 +size 40072 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png new file mode 100644 index 0000000000..d2f9e318e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b431eae85dcb3e5efde883a47c78aab6c346429366357bb5f6e740342eeeb97 +size 40072 From 55f2531af14fb02ce93e212fd3172a73ffbb32a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Oct 2025 11:21:36 +0200 Subject: [PATCH 09/13] Remove code duplication. --- .../impl/roomlist/RoomListStateProvider.kt | 14 +------------ .../impl/roomlist/RoomListPresenterTest.kt | 1 + .../AcceptDeclineInviteStateProvider.kt | 21 +++++++++++++++++++ .../AcceptDeclineInviteStateProvider.kt | 12 +---------- .../joinroom/impl/JoinRoomStateProvider.kt | 12 +---------- .../joinroom/impl/JoinRoomPresenterTest.kt | 1 + .../space/impl/root/SpaceStateProvider.kt | 12 +---------- .../space/impl/root/SpacePresenterTest.kt | 1 + 8 files changed, 28 insertions(+), 46 deletions(-) create mode 100644 features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt index 55fc8948f6..089e5c23a5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt @@ -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 = AsyncAction.Uninitialized, - declineAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (AcceptDeclineInviteEvents) -> Unit = {} -) = AcceptDeclineInviteState( - acceptAction = acceptAction, - declineAction = declineAction, - eventSink = eventSink, -) - internal fun aRoomListRoomSummaryList(): ImmutableList { return persistentListOf( aRoomListRoomSummary( diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 044c150ad2..7ad58f2832 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -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 diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt new file mode 100644 index 0000000000..bd5c5e6749 --- /dev/null +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt @@ -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 = AsyncAction.Uninitialized, + declineAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AcceptDeclineInviteEvents) -> Unit = {}, +) = AcceptDeclineInviteState( + acceptAction = acceptAction, + declineAction = declineAction, + eventSink = eventSink, +) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt index 9896de1d3e..6db000d3db 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt @@ -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 = AsyncAction.Uninitialized, - declineAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (AcceptDeclineInviteEvents) -> Unit = {} -) = AcceptDeclineInviteState( - acceptAction = acceptAction, - declineAction = declineAction, - eventSink = eventSink, -) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 8eeecab83c..d0af006dd7 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -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 = AsyncAction.Uninitialized, - declineAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (AcceptDeclineInviteEvents) -> Unit = {} -) = AcceptDeclineInviteState( - acceptAction = acceptAction, - declineAction = declineAction, - eventSink = eventSink, -) - internal fun anInviteSender( userId: UserId = UserId("@bob:domain"), displayName: String = "Bob", diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 574c310d1f..10b5c1a712 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -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 diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 467ec6ae2b..1e7444f498 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -8,8 +8,8 @@ package io.element.android.features.space.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -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.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership @@ -69,16 +69,6 @@ fun aSpaceState( eventSink = {} ) -internal fun anAcceptDeclineInviteState( - acceptAction: AsyncAction = AsyncAction.Uninitialized, - declineAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (AcceptDeclineInviteEvents) -> Unit = {} -) = AcceptDeclineInviteState( - acceptAction = acceptAction, - declineAction = declineAction, - eventSink = eventSink, -) - private fun aListOfSpaceRooms(): List { return listOf( aSpaceRoom( diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 5845527223..2e6e5a7600 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -12,6 +12,7 @@ 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.AcceptDeclineInviteState +import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.Presenter From b9638fb3969c7c00aac55d4531494f8b14be0eb3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Oct 2025 11:22:46 +0200 Subject: [PATCH 10/13] Add modifier parameter. --- .../android/libraries/matrix/ui/components/JoinButton.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt index 1de19570f9..0d5b94dc64 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/JoinButton.kt @@ -10,6 +10,7 @@ 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 @@ -20,9 +21,11 @@ import io.element.android.libraries.ui.strings.CommonStrings 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, From 145306230acbcd7a038e093ed7b2b8170a2b7a42 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Oct 2025 11:44:20 +0200 Subject: [PATCH 11/13] Add unit test on SpaceState --- .../space/impl/root/SpaceStateProvider.kt | 3 +- .../space/impl/root/SpaceStateTest.kt | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index 1e7444f498..b92dfb2bc9 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -55,6 +55,7 @@ fun aSpaceState( children: List = emptyList(), seenSpaceInvites: Set = emptySet(), joiningRooms: Set = emptySet(), + joinActions: Map> = joiningRooms.associateWith { AsyncAction.Loading }, hideInvitesAvatar: Boolean = false, hasMoreToLoad: Boolean = false, acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), @@ -64,7 +65,7 @@ fun aSpaceState( seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, hasMoreToLoad = hasMoreToLoad, - joinActions = joiningRooms.associateWith { AsyncAction.Loading }.toImmutableMap(), + joinActions = joinActions.toImmutableMap(), acceptDeclineInviteState = acceptDeclineInviteState, eventSink = {} ) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt new file mode 100644 index 0000000000..d036d7023c --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceStateTest.kt @@ -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() + } +} From 97ab7a53580c8709d191a1fe6db303507aa249e0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Oct 2025 12:27:48 +0200 Subject: [PATCH 12/13] Add unit test on SpacePresenter --- .../space/impl/root/SpacePresenterTest.kt | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 2e6e5a7600..9a40a8f18b 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -11,27 +11,39 @@ 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.features.space.api.SpaceEntryPoint +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.matrix.test.spaces.FakeSpaceService 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 @@ -57,6 +69,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() } @@ -159,6 +173,180 @@ class SpacePresenterTest { } } + @Test + fun `present - join a room success`() = runTest { + val joinRoom = lambdaRecorder, AnalyticsJoinedRoom.Trigger, Result> { _, _, _ -> + 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( + client = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { 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( + client = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { 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() + 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( + client = FakeMatrixClient( + spaceService = FakeSpaceService( + spaceRoomListResult = { 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( inputs: SpaceEntryPoint.Inputs = SpaceEntryPoint.Inputs(A_ROOM_ID), client: MatrixClient = FakeMatrixClient(), From 53bcb1e23a74c5087ed6c222450a7ee397c65c1d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Oct 2025 14:29:21 +0200 Subject: [PATCH 13/13] Add unit test on SpaceView --- .../space/impl/root/SpaceStateProvider.kt | 3 +- .../features/space/impl/root/SpaceViewTest.kt | 130 ++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index b92dfb2bc9..c0a88c38f5 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -59,6 +59,7 @@ fun aSpaceState( hideInvitesAvatar: Boolean = false, hasMoreToLoad: Boolean = false, acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + eventSink: (SpaceEvents) -> Unit = { }, ) = SpaceState( currentSpace = parentSpace, children = children.toImmutableList(), @@ -67,7 +68,7 @@ fun aSpaceState( hasMoreToLoad = hasMoreToLoad, joinActions = joinActions.toImmutableMap(), acceptDeclineInviteState = acceptDeclineInviteState, - eventSink = {} + eventSink = eventSink, ) private fun aListOfSpaceRooms(): List { diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt new file mode 100644 index 0000000000..f95b4e6514 --- /dev/null +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -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() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(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(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() + 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() + 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() + rule.setSpaceView( + aSpaceState( + children = listOf(aSpaceRoom), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) + } +} + +private fun AndroidComposeTestRule.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, + ) + } +}