Skip to content

Commit 014061f

Browse files
authored
Merge pull request #2674 from element-hq/feature/bma/roomSuggestion
Room / User suggestions
2 parents 8128a59 + 16d289e commit 014061f

File tree

34 files changed

+573
-53
lines changed

34 files changed

+573
-53
lines changed

appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import io.element.android.libraries.architecture.NodeInputs
4040
import io.element.android.libraries.architecture.inputs
4141
import io.element.android.libraries.di.DaggerComponentOwner
4242
import io.element.android.libraries.di.SessionScope
43+
import io.element.android.libraries.matrix.api.MatrixClient
4344
import io.element.android.libraries.matrix.api.core.RoomId
4445
import io.element.android.libraries.matrix.api.core.UserId
4546
import io.element.android.libraries.matrix.api.room.MatrixRoom
@@ -61,6 +62,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
6162
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
6263
private val appNavigationStateService: AppNavigationStateService,
6364
private val appCoroutineScope: CoroutineScope,
65+
private val matrixClient: MatrixClient,
6466
roomComponentFactory: RoomComponentFactory,
6567
roomMembershipObserver: RoomMembershipObserver,
6668
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>(
@@ -92,6 +94,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
9294
Timber.v("OnCreate => ${inputs.room.roomId}")
9395
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
9496
fetchRoomMembers()
97+
trackVisitedRoom()
9598
},
9699
onResume = {
97100
appCoroutineScope.launch {
@@ -117,6 +120,10 @@ class RoomLoadedFlowNode @AssistedInject constructor(
117120
inputs<Inputs>()
118121
}
119122

123+
private fun trackVisitedRoom() = lifecycleScope.launch {
124+
matrixClient.trackRecentlyVisitedRoom(inputs.room.roomId)
125+
}
126+
120127
private fun fetchRoomMembers() = lifecycleScope.launch {
121128
inputs.room.updateMembers()
122129
}

appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
3333
import io.element.android.libraries.architecture.childNode
3434
import io.element.android.libraries.matrix.api.room.MatrixRoom
3535
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
36+
import io.element.android.libraries.matrix.test.FakeMatrixClient
3637
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
3738
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
3839
import kotlinx.coroutines.CoroutineScope
@@ -101,6 +102,7 @@ class RoomFlowNodeTest {
101102
roomMembershipObserver = RoomMembershipObserver(),
102103
appCoroutineScope = coroutineScope,
103104
roomComponentFactory = FakeRoomComponentFactory(),
105+
matrixClient = FakeMatrixClient(),
104106
)
105107

106108
@Test

changelog.d/2634.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show users from last visited DM as suggestion when starting a Chat or when creating a Room.

features/createroom/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ dependencies {
6969
testImplementation(projects.libraries.usersearch.test)
7070
testImplementation(projects.features.createroom.test)
7171
testImplementation(projects.tests.testutils)
72+
testImplementation(libs.androidx.compose.ui.test.junit)
73+
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
7274

7375
ksp(libs.showkase.processor)
7476
}

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.addpeople
1919
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
2020
import io.element.android.features.createroom.impl.userlist.SelectionMode
2121
import io.element.android.features.createroom.impl.userlist.UserListState
22+
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
2223
import io.element.android.features.createroom.impl.userlist.aUserListState
2324
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
2425
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
@@ -29,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
2930
override val values: Sequence<UserListState>
3031
get() = sequenceOf(
3132
aUserListState(),
32-
aUserListState().copy(
33+
aUserListState(
3334
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
3435
selectedUsers = aMatrixUserList().toImmutableList(),
3536
isSearchActive = false,
3637
selectionMode = SelectionMode.Multiple,
3738
),
38-
aUserListState().copy(
39+
aUserListState(
3940
searchResults = SearchBarResultState.Results(
4041
aMatrixUserList()
4142
.mapIndexed { index, matrixUser ->
@@ -46,6 +47,9 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
4647
selectedUsers = aMatrixUserList().toImmutableList(),
4748
isSearchActive = true,
4849
selectionMode = SelectionMode.Multiple,
49-
)
50+
),
51+
aUserListState(
52+
recentDirectRooms = aRecentDirectRoomList(),
53+
),
5054
)
5155
}

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@
1616

1717
package io.element.android.features.createroom.impl.addpeople
1818

19-
import androidx.compose.foundation.layout.Column
2019
import androidx.compose.foundation.layout.consumeWindowInsets
2120
import androidx.compose.foundation.layout.fillMaxSize
22-
import androidx.compose.foundation.layout.fillMaxWidth
2321
import androidx.compose.foundation.layout.padding
2422
import androidx.compose.material3.ExperimentalMaterial3Api
2523
import androidx.compose.runtime.Composable
@@ -64,21 +62,16 @@ fun AddPeopleView(
6462
)
6563
}
6664
) { padding ->
67-
Column(
65+
UserListView(
6866
modifier = Modifier
6967
.fillMaxSize()
7068
.padding(padding)
7169
.consumeWindowInsets(padding),
72-
) {
73-
UserListView(
74-
modifier = Modifier
75-
.fillMaxWidth(),
76-
state = state,
77-
showBackButton = false,
78-
onUserSelected = { },
79-
onUserDeselected = {},
80-
)
81-
}
70+
state = state,
71+
showBackButton = false,
72+
onUserSelected = {},
73+
onUserDeselected = {},
74+
)
8275
}
8376
}
8477

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,27 @@ package io.element.android.features.createroom.impl.components
1919
import androidx.compose.foundation.layout.Column
2020
import androidx.compose.foundation.layout.PaddingValues
2121
import androidx.compose.foundation.layout.fillMaxWidth
22+
import androidx.compose.foundation.lazy.LazyColumn
2223
import androidx.compose.runtime.Composable
2324
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.res.stringResource
2426
import androidx.compose.ui.tooling.preview.PreviewParameter
2527
import androidx.compose.ui.unit.dp
2628
import io.element.android.features.createroom.impl.userlist.UserListEvents
2729
import io.element.android.features.createroom.impl.userlist.UserListState
2830
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
31+
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
2932
import io.element.android.libraries.designsystem.preview.ElementPreview
3033
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
34+
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
35+
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
3136
import io.element.android.libraries.matrix.api.user.MatrixUser
37+
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
38+
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
3239
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
40+
import io.element.android.libraries.matrix.ui.model.getAvatarData
41+
import io.element.android.libraries.matrix.ui.model.getBestName
42+
import io.element.android.libraries.ui.strings.CommonStrings
3343

3444
@Composable
3545
fun UserListView(
@@ -74,6 +84,43 @@ fun UserListView(
7484
},
7585
)
7686
}
87+
if (!state.isSearchActive && state.recentDirectRooms.isNotEmpty()) {
88+
LazyColumn {
89+
item {
90+
ListSectionHeader(
91+
title = stringResource(id = CommonStrings.common_suggestions),
92+
hasDivider = false,
93+
)
94+
}
95+
state.recentDirectRooms.forEachIndexed { index, recentDirectRoom ->
96+
item {
97+
val isSelected = state.selectedUsers.any {
98+
recentDirectRoom.matrixUser.userId == it.userId
99+
}
100+
CheckableUserRow(
101+
checked = isSelected,
102+
onCheckedChange = {
103+
if (isSelected) {
104+
state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser))
105+
onUserDeselected(recentDirectRoom.matrixUser)
106+
} else {
107+
state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser))
108+
onUserSelected(recentDirectRoom.matrixUser)
109+
}
110+
},
111+
data = CheckableUserRowData.Resolved(
112+
avatarData = recentDirectRoom.matrixUser.getAvatarData(AvatarSize.UserListItem),
113+
name = recentDirectRoom.matrixUser.getBestName(),
114+
subtext = recentDirectRoom.matrixUser.userId.value,
115+
),
116+
)
117+
if (index < state.recentDirectRooms.lastIndex) {
118+
HorizontalDivider()
119+
}
120+
}
121+
}
122+
}
123+
}
77124
}
78125
}
79126

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
package io.element.android.features.createroom.impl.root
1818

1919
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
20+
import io.element.android.features.createroom.impl.userlist.UserListState
21+
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
2022
import io.element.android.features.createroom.impl.userlist.aUserListState
2123
import io.element.android.libraries.architecture.AsyncAction
2224
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
25+
import io.element.android.libraries.matrix.api.core.RoomId
2326
import io.element.android.libraries.matrix.ui.components.aMatrixUser
2427
import io.element.android.libraries.usersearch.api.UserSearchResult
2528
import kotlinx.collections.immutable.persistentListOf
@@ -28,7 +31,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
2831
override val values: Sequence<CreateRoomRootState>
2932
get() = sequenceOf(
3033
aCreateRoomRootState(),
31-
aCreateRoomRootState().copy(
34+
aCreateRoomRootState(
3235
startDmAction = AsyncAction.Loading,
3336
userListState = aMatrixUser().let {
3437
aUserListState().copy(
@@ -39,7 +42,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
3942
)
4043
}
4144
),
42-
aCreateRoomRootState().copy(
45+
aCreateRoomRootState(
4346
startDmAction = AsyncAction.Failure(Throwable("error")),
4447
userListState = aMatrixUser().let {
4548
aUserListState().copy(
@@ -50,12 +53,22 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
5053
)
5154
}
5255
),
56+
aCreateRoomRootState(
57+
userListState = aUserListState(
58+
recentDirectRooms = aRecentDirectRoomList()
59+
)
60+
),
5361
)
5462
}
5563

56-
fun aCreateRoomRootState() = CreateRoomRootState(
57-
eventSink = {},
58-
applicationName = "Element X Preview",
59-
startDmAction = AsyncAction.Uninitialized,
60-
userListState = aUserListState(),
64+
fun aCreateRoomRootState(
65+
applicationName: String = "Element X Preview",
66+
userListState: UserListState = aUserListState(),
67+
startDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
68+
eventSink: (CreateRoomRootEvents) -> Unit = {},
69+
) = CreateRoomRootState(
70+
applicationName = applicationName,
71+
userListState = userListState,
72+
startDmAction = startDmAction,
73+
eventSink = eventSink,
6174
)

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
2626
import androidx.compose.foundation.layout.height
2727
import androidx.compose.foundation.layout.padding
2828
import androidx.compose.foundation.layout.size
29+
import androidx.compose.foundation.lazy.LazyColumn
2930
import androidx.compose.material3.ExperimentalMaterial3Api
3031
import androidx.compose.material3.MaterialTheme
3132
import androidx.compose.runtime.Composable
@@ -46,11 +47,14 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
4647
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
4748
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
4849
import io.element.android.libraries.designsystem.theme.components.Icon
50+
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
4951
import io.element.android.libraries.designsystem.theme.components.Scaffold
5052
import io.element.android.libraries.designsystem.theme.components.Text
5153
import io.element.android.libraries.designsystem.theme.components.TopAppBar
5254
import io.element.android.libraries.matrix.api.core.RoomId
55+
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
5356
import io.element.android.libraries.ui.strings.CommonStrings
57+
import kotlinx.collections.immutable.persistentListOf
5458

5559
@Composable
5660
fun CreateRoomRootView(
@@ -77,7 +81,11 @@ fun CreateRoomRootView(
7781
) {
7882
UserListView(
7983
modifier = Modifier.fillMaxWidth(),
80-
state = state.userListState,
84+
// Do not render suggestions in this case, the suggestion will be rendered
85+
// by CreateRoomActionButtonsList
86+
state = state.userListState.copy(
87+
recentDirectRooms = persistentListOf(),
88+
),
8189
onUserSelected = {
8290
state.eventSink(CreateRoomRootEvents.StartDM(it))
8391
},
@@ -89,6 +97,7 @@ fun CreateRoomRootView(
8997
state = state,
9098
onNewRoomClicked = onNewRoomClicked,
9199
onInvitePeopleClicked = onInviteFriendsClicked,
100+
onDmClicked = onOpenDM,
92101
)
93102
}
94103
}
@@ -106,7 +115,7 @@ fun CreateRoomRootView(
106115
onRetry = {
107116
state.userListState.selectedUsers.firstOrNull()
108117
?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
109-
// Cancel start DM if there is no more selected user (should not happen)
118+
// Cancel start DM if there is no more selected user (should not happen)
110119
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
111120
},
112121
onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
@@ -139,18 +148,43 @@ private fun CreateRoomActionButtonsList(
139148
state: CreateRoomRootState,
140149
onNewRoomClicked: () -> Unit,
141150
onInvitePeopleClicked: () -> Unit,
151+
onDmClicked: (RoomId) -> Unit,
142152
) {
143-
Column {
144-
CreateRoomActionButton(
145-
iconRes = CompoundDrawables.ic_compound_plus,
146-
text = stringResource(id = R.string.screen_create_room_action_create_room),
147-
onClick = onNewRoomClicked,
148-
)
149-
CreateRoomActionButton(
150-
iconRes = CompoundDrawables.ic_compound_share_android,
151-
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
152-
onClick = onInvitePeopleClicked,
153-
)
153+
LazyColumn {
154+
item {
155+
CreateRoomActionButton(
156+
iconRes = CompoundDrawables.ic_compound_plus,
157+
text = stringResource(id = R.string.screen_create_room_action_create_room),
158+
onClick = onNewRoomClicked,
159+
)
160+
}
161+
item {
162+
CreateRoomActionButton(
163+
iconRes = CompoundDrawables.ic_compound_share_android,
164+
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
165+
onClick = onInvitePeopleClicked,
166+
)
167+
}
168+
if (state.userListState.recentDirectRooms.isNotEmpty()) {
169+
item {
170+
ListSectionHeader(
171+
title = stringResource(id = CommonStrings.common_suggestions),
172+
hasDivider = false,
173+
)
174+
}
175+
state.userListState.recentDirectRooms.forEach { recentDirectRoom ->
176+
item {
177+
MatrixUserRow(
178+
modifier = Modifier.clickable(
179+
onClick = {
180+
onDmClicked(recentDirectRoom.roomId)
181+
}
182+
),
183+
matrixUser = recentDirectRoom.matrixUser,
184+
)
185+
}
186+
}
187+
}
154188
}
155189
}
156190

0 commit comments

Comments
 (0)