Skip to content

Commit be8455b

Browse files
authored
Merge pull request #5493 from element-hq/feature/fga/space_description
feature(space): make sure to handle topic properly
2 parents 5852e64 + 59c2a95 commit be8455b

24 files changed

+238
-56
lines changed

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ sealed interface SpaceEvents {
1515
data object ClearFailures : SpaceEvents
1616
data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents
1717
data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents
18+
19+
data class ShowTopicViewer(val topic: String) : SpaceEvents
20+
data object HideTopicViewer : SpaceEvents
1821
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class SpacePresenter(
8181
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
8282
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
8383

84+
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
85+
8486
LaunchedEffect(children) {
8587
// Remove joined children from the join actions
8688
val joinedChildren = children
@@ -113,6 +115,8 @@ class SpacePresenter(
113115
AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false)
114116
)
115117
}
118+
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
119+
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
116120
}
117121
}
118122
return SpaceState(
@@ -123,6 +127,7 @@ class SpacePresenter(
123127
hasMoreToLoad = hasMoreToLoad,
124128
joinActions = joinActions.toImmutableMap(),
125129
acceptDeclineInviteState = acceptDeclineInviteState,
130+
topicViewerState = topicViewerState,
126131
eventSink = ::handleEvents,
127132
)
128133
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package io.element.android.features.space.impl.root
99

10+
import androidx.compose.runtime.Immutable
1011
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
1112
import io.element.android.libraries.architecture.AsyncAction
1213
import io.element.android.libraries.matrix.api.core.RoomId
@@ -23,10 +24,17 @@ data class SpaceState(
2324
val hasMoreToLoad: Boolean,
2425
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
2526
val acceptDeclineInviteState: AcceptDeclineInviteState,
27+
val topicViewerState: TopicViewerState,
2628
val eventSink: (SpaceEvents) -> Unit
2729
) {
2830
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
2931
val hasAnyFailure: Boolean = joinActions.values.any {
3032
it is AsyncAction.Failure
3133
}
3234
}
35+
36+
@Immutable
37+
sealed interface TopicViewerState {
38+
data object Hidden : TopicViewerState
39+
data class Shown(val topic: String) : TopicViewerState
40+
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.features.space.impl.root
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
1112
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
1213
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
1314
import io.element.android.libraries.architecture.AsyncAction
@@ -25,51 +26,32 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
2526
override val values: Sequence<SpaceState>
2627
get() = sequenceOf(
2728
aSpaceState(),
29+
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Public)),
30+
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Restricted(persistentListOf()))),
31+
aSpaceState(children = aListOfSpaceRooms()),
2832
aSpaceState(
29-
parentSpace = aSpaceRoom(
30-
joinRule = JoinRule.Public
31-
)
32-
),
33-
aSpaceState(
34-
parentSpace = aSpaceRoom(
35-
joinRule = JoinRule.Restricted(persistentListOf())
36-
)
37-
),
38-
aSpaceState(
39-
parentSpace = aSpaceRoom(
40-
numJoinedMembers = 5,
41-
childrenCount = 10,
42-
worldReadable = true,
43-
),
44-
hasMoreToLoad = true,
45-
),
46-
aSpaceState(
47-
hasMoreToLoad = true,
33+
parentSpace = aParentSpace(),
4834
children = aListOfSpaceRooms(),
35+
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
36+
hasMoreToLoad = false
4937
),
5038
aSpaceState(
51-
hasMoreToLoad = false,
52-
children = aListOfSpaceRooms(),
53-
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
54-
)
39+
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
40+
),
5541
// Add other states here
5642
)
5743
}
5844

5945
fun aSpaceState(
60-
parentSpace: SpaceRoom? = aSpaceRoom(
61-
numJoinedMembers = 5,
62-
childrenCount = 10,
63-
worldReadable = true,
64-
roomId = RoomId("!spaceId0:example.com"),
65-
),
46+
parentSpace: SpaceRoom? = aParentSpace(),
6647
children: List<SpaceRoom> = emptyList(),
6748
seenSpaceInvites: Set<RoomId> = emptySet(),
6849
joiningRooms: Set<RoomId> = emptySet(),
6950
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
7051
hideInvitesAvatar: Boolean = false,
71-
hasMoreToLoad: Boolean = false,
52+
hasMoreToLoad: Boolean = true,
7253
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
54+
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
7355
eventSink: (SpaceEvents) -> Unit = { },
7456
) = SpaceState(
7557
currentSpace = parentSpace,
@@ -79,9 +61,23 @@ fun aSpaceState(
7961
hasMoreToLoad = hasMoreToLoad,
8062
joinActions = joinActions.toImmutableMap(),
8163
acceptDeclineInviteState = acceptDeclineInviteState,
64+
topicViewerState = topicViewerState,
8265
eventSink = eventSink,
8366
)
8467

68+
private fun aParentSpace(
69+
joinRule: JoinRule? = null,
70+
): SpaceRoom {
71+
return aSpaceRoom(
72+
numJoinedMembers = 5,
73+
childrenCount = 10,
74+
worldReadable = true,
75+
joinRule = joinRule,
76+
roomId = RoomId("!spaceId0:example.com"),
77+
topic = "Space description goes here. " + LoremIpsum(20).values.first(),
78+
)
79+
}
80+
8581
private fun aListOfSpaceRooms(): List<SpaceRoom> {
8682
return listOf(
8783
aSpaceRoom(

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package io.element.android.features.space.impl.root
99

10+
import androidx.compose.foundation.interaction.MutableInteractionSource
1011
import androidx.compose.foundation.layout.Box
1112
import androidx.compose.foundation.layout.Row
1213
import androidx.compose.foundation.layout.fillMaxSize
@@ -33,6 +34,8 @@ import androidx.compose.ui.unit.dp
3334
import io.element.android.compound.theme.ElementTheme
3435
import io.element.android.compound.tokens.generated.CompoundIcons
3536
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
37+
import io.element.android.libraries.designsystem.components.ClickableLinkText
38+
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
3639
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
3740
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
3841
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
@@ -61,6 +64,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
6164
import kotlinx.collections.immutable.toImmutableList
6265
import kotlinx.coroutines.delay
6366

67+
@OptIn(ExperimentalMaterial3Api::class)
6468
@Composable
6569
fun SpaceView(
6670
state: SpaceState,
@@ -87,7 +91,10 @@ fun SpaceView(
8791
) {
8892
SpaceViewContent(
8993
state = state,
90-
onRoomClick = onRoomClick
94+
onRoomClick = onRoomClick,
95+
onTopicClick = { topic ->
96+
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
97+
}
9198
)
9299
JoinRoomFailureEffect(
93100
hasAnyFailure = state.hasAnyFailure,
@@ -97,6 +104,14 @@ fun SpaceView(
97104
}
98105
},
99106
)
107+
if (state.topicViewerState is TopicViewerState.Shown) {
108+
TopicViewerBottomSheet(
109+
topicViewerState = state.topicViewerState,
110+
onDismiss = {
111+
state.eventSink(SpaceEvents.HideTopicViewer)
112+
}
113+
)
114+
}
100115
}
101116

102117
@Composable
@@ -120,10 +135,31 @@ private fun JoinRoomFailureEffect(
120135
}
121136
}
122137

138+
@Composable
139+
private fun TopicViewerBottomSheet(
140+
topicViewerState: TopicViewerState.Shown,
141+
onDismiss: () -> Unit,
142+
modifier: Modifier = Modifier,
143+
) {
144+
SimpleModalBottomSheet(
145+
title = stringResource(CommonStrings.common_description),
146+
onDismiss = onDismiss,
147+
modifier = modifier
148+
) {
149+
ClickableLinkText(
150+
text = topicViewerState.topic,
151+
interactionSource = remember { MutableInteractionSource() },
152+
style = ElementTheme.typography.fontBodyMdRegular,
153+
color = ElementTheme.colors.textSecondary,
154+
)
155+
}
156+
}
157+
123158
@Composable
124159
private fun SpaceViewContent(
125160
state: SpaceState,
126161
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
162+
onTopicClick: (String) -> Unit,
127163
modifier: Modifier = Modifier,
128164
) {
129165
LazyColumn(modifier.fillMaxSize()) {
@@ -134,9 +170,11 @@ private fun SpaceViewContent(
134170
avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
135171
name = currentSpace.displayName,
136172
topic = currentSpace.topic,
173+
topicMaxLines = 2,
137174
visibility = currentSpace.visibility,
138175
heroes = currentSpace.heroes.toImmutableList(),
139176
numberOfMembers = currentSpace.numJoinedMembers,
177+
onTopicClick = onTopicClick
140178
)
141179
}
142180
}

features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class SpacePresenterTest {
6060
assertThat(state.hasMoreToLoad).isTrue()
6161
assertThat(state.joinActions).isEmpty()
6262
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
63+
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
6364
advanceUntilIdle()
6465
paginateResult.assertions().isCalledOnce()
6566
}
@@ -236,6 +237,24 @@ class SpacePresenterTest {
236237
}
237238
}
238239

240+
@Test
241+
fun `present - topic viewer state`() = runTest {
242+
val paginateResult = lambdaRecorder<Result<Unit>> {
243+
Result.success(Unit)
244+
}
245+
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
246+
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
247+
presenter.test {
248+
val state = awaitItem()
249+
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
250+
advanceUntilIdle()
251+
state.eventSink(SpaceEvents.ShowTopicViewer("topic"))
252+
assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Shown("topic"))
253+
state.eventSink(SpaceEvents.HideTopicViewer)
254+
assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Hidden)
255+
}
256+
}
257+
239258
@Test
240259
fun `present - accept invite is transmitted to acceptDeclineInviteState`() {
241260
`invite action is transmitted to acceptDeclineInviteState`(

features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
1818
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
1919
import io.element.android.libraries.matrix.test.A_ROOM_ID
2020
import io.element.android.libraries.matrix.test.A_ROOM_NAME
21+
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
2122
import io.element.android.libraries.previewutils.room.aSpaceRoom
2223
import io.element.android.libraries.ui.strings.CommonStrings
2324
import io.element.android.tests.testutils.EnsureNeverCalled
@@ -31,6 +32,7 @@ import org.junit.Rule
3132
import org.junit.Test
3233
import org.junit.rules.TestRule
3334
import org.junit.runner.RunWith
35+
import org.robolectric.annotation.Config
3436

3537
@RunWith(AndroidJUnit4::class)
3638
class SpaceViewTest {
@@ -42,6 +44,7 @@ class SpaceViewTest {
4244
ensureCalledOnce {
4345
rule.setSpaceView(
4446
aSpaceState(
47+
hasMoreToLoad = false,
4548
eventSink = eventsRecorder,
4649
),
4750
onBackClick = it,
@@ -58,6 +61,7 @@ class SpaceViewTest {
5861
rule.setSpaceView(
5962
aSpaceState(
6063
children = listOf(aSpaceRoom),
64+
hasMoreToLoad = false,
6165
eventSink = eventsRecorder,
6266
),
6367
onRoomClick = it,
@@ -73,19 +77,22 @@ class SpaceViewTest {
7377
rule.setSpaceView(
7478
aSpaceState(
7579
children = listOf(aSpaceRoom),
80+
hasMoreToLoad = false,
7681
eventSink = eventsRecorder,
7782
),
7883
)
7984
rule.clickOn(CommonStrings.action_join)
8085
eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom))
8186
}
8287

88+
@Config(qualifiers = "h1024dp")
8389
@Test
8490
fun `clicking on accept invite emits the expected Event`() {
8591
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
8692
val eventsRecorder = EventsRecorder<SpaceEvents>()
8793
rule.setSpaceView(
8894
aSpaceState(
95+
hasMoreToLoad = false,
8996
children = listOf(aSpaceRoom),
9097
eventSink = eventsRecorder,
9198
),
@@ -94,19 +101,36 @@ class SpaceViewTest {
94101
eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom))
95102
}
96103

104+
@Config(qualifiers = "h1024dp")
97105
@Test
98106
fun `clicking on decline invite emits the expected Event`() {
99107
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
100108
val eventsRecorder = EventsRecorder<SpaceEvents>()
101109
rule.setSpaceView(
102110
aSpaceState(
111+
hasMoreToLoad = false,
103112
children = listOf(aSpaceRoom),
104113
eventSink = eventsRecorder,
105114
),
106115
)
107116
rule.clickOn(CommonStrings.action_decline)
108117
eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom))
109118
}
119+
120+
@Config(qualifiers = "h1024dp")
121+
@Test
122+
fun `clicking on topic emits the expected Event`() {
123+
val eventsRecorder = EventsRecorder<SpaceEvents>()
124+
rule.setSpaceView(
125+
aSpaceState(
126+
parentSpace = aSpaceRoom(topic = A_ROOM_TOPIC),
127+
hasMoreToLoad = false,
128+
eventSink = eventsRecorder,
129+
)
130+
)
131+
rule.onNodeWithText(A_ROOM_TOPIC).performClick()
132+
eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
133+
}
110134
}
111135

112136
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(

0 commit comments

Comments
 (0)