Skip to content

Commit d87cf5c

Browse files
committed
Add RoomMembershipDetails to get the room member info for the current user and the sender of its m.room.member state event in the room.
1 parent fa5ee41 commit d87cf5c

File tree

11 files changed

+101
-17
lines changed

11 files changed

+101
-17
lines changed

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.exception.ErrorKind
4343
import io.element.android.libraries.matrix.api.getRoomInfoFlow
4444
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
4545
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
46+
import io.element.android.libraries.matrix.api.room.RoomMember
4647
import io.element.android.libraries.matrix.api.room.RoomType
4748
import io.element.android.libraries.matrix.api.room.isDm
4849
import io.element.android.libraries.matrix.api.room.join.JoinRoom
@@ -97,7 +98,20 @@ class JoinRoomPresenter @AssistedInject constructor(
9798
when {
9899
isDismissingContent -> value = ContentState.Dismissing
99100
roomInfo.isPresent -> {
100-
value = roomInfo.get().toContentState()
101+
val (sender, reason) = when (roomInfo.get().currentUserMembership) {
102+
CurrentUserMembership.BANNED -> {
103+
// Workaround to get info about the sender for banned rooms
104+
// TODO re-do this once we have a better API in the SDK
105+
val preview = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
106+
val membershipDetalis = preview.getOrNull()?.membershipDetails()?.getOrNull()
107+
membershipDetalis?.senderMember to membershipDetalis?.currentUserMember?.membershipChangeReason
108+
}
109+
CurrentUserMembership.INVITED -> {
110+
roomInfo.get().inviter to null
111+
}
112+
else -> null to null
113+
}
114+
value = roomInfo.get().toContentState(sender, reason)
101115
}
102116
roomDescription.isPresent -> {
103117
value = roomDescription.get().toContentState()
@@ -106,10 +120,19 @@ class JoinRoomPresenter @AssistedInject constructor(
106120
value = ContentState.Loading
107121
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
108122
value = result.fold(
109-
onSuccess = { previewInfo ->
110-
previewInfo.toContentState()
111123
onSuccess = { preview ->
112-
preview.info.toContentState()
124+
val membershipInfo = when (preview.info.membership) {
125+
CurrentUserMembership.INVITED,
126+
CurrentUserMembership.BANNED,
127+
CurrentUserMembership.KNOCKED -> {
128+
preview.membershipDetails().getOrNull()
129+
}
130+
else -> null
131+
}
132+
preview.info.toContentState(
133+
senderMember = membershipInfo?.senderMember,
134+
reason = membershipInfo?.currentUserMember?.membershipChangeReason,
135+
)
113136
},
114137
onFailure = { throwable ->
115138
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
@@ -213,7 +236,7 @@ class JoinRoomPresenter @AssistedInject constructor(
213236
}
214237
}
215238

216-
private fun RoomPreviewInfo.toContentState(): ContentState {
239+
private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState {
217240
return ContentState.Loaded(
218241
roomId = roomId,
219242
name = name,
@@ -224,8 +247,8 @@ private fun RoomPreviewInfo.toContentState(): ContentState {
224247
roomType = roomType,
225248
roomAvatarUrl = avatarUrl,
226249
joinAuthorisationStatus = when (membership) {
227-
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(null)
228-
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(null)
250+
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(senderMember?.toInviteSender())
251+
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
229252
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
230253
else -> joinRule.toJoinAuthorisationStatus()
231254
}
@@ -252,7 +275,7 @@ internal fun RoomDescription.toContentState(): ContentState {
252275
}
253276

254277
@VisibleForTesting
255-
internal fun MatrixRoomInfo.toContentState(): ContentState {
278+
internal fun MatrixRoomInfo.toContentState(membershipSender: RoomMember?, reason: String?): ContentState {
256279
return ContentState.Loaded(
257280
roomId = id,
258281
name = name,
@@ -264,10 +287,11 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
264287
roomAvatarUrl = avatarUrl,
265288
joinAuthorisationStatus = when (currentUserMembership) {
266289
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
267-
inviteSender = inviter?.toInviteSender()
290+
inviteSender = membershipSender?.toInviteSender()
268291
)
269292
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
270-
banSender = inviter?.toInviteSender()
293+
banSender = membershipSender?.toInviteSender(),
294+
reason = reason,
271295
)
272296
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
273297
else -> joinRule.toJoinAuthorisationStatus()

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ sealed interface JoinAuthorisationStatus {
9393
data object None : JoinAuthorisationStatus
9494
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
9595
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
96-
data class IsBanned(val banSender: InviteSender?) : JoinAuthorisationStatus
96+
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
9797
data object IsKnocked : JoinAuthorisationStatus
9898
data object CanKnock : JoinAuthorisationStatus
9999
data object CanJoin : JoinAuthorisationStatus

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
115115
contentState = aLoadedContentState(
116116
name = "A banned room",
117117
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
118-
InviteSender(
118+
banSender = InviteSender(
119119
userId = UserId("@alice:domain"),
120120
displayName = "Alice",
121121
avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender),
122122
membershipChangeReason = "spamming"
123-
)
123+
),
124+
reason = "spamming",
124125
),
125126
)
126127
),

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ private fun JoinBannedFooter(
287287
modifier: Modifier = Modifier,
288288
) {
289289
Column(modifier = modifier) {
290-
val banReason = status.banSender?.membershipChangeReason?.let {
291-
stringResource(R.string.screen_join_room_ban_reason, it)
290+
val banReason = status.reason?.let {
291+
stringResource(R.string.screen_join_room_ban_reason, it.removeSuffix("."))
292292
}
293293
val title = if (status.banSender != null) {
294294
stringResource(R.string.screen_join_room_ban_by_message, status.banSender.displayName)

features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
2828
import io.element.android.libraries.matrix.api.exception.ClientException
2929
import io.element.android.libraries.matrix.api.exception.ErrorKind
3030
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
31+
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
3132
import io.element.android.libraries.matrix.api.room.RoomType
3233
import io.element.android.libraries.matrix.api.room.join.JoinRule
3334
import io.element.android.libraries.matrix.test.AN_EXCEPTION
3435
import io.element.android.libraries.matrix.test.A_ROOM_ID
3536
import io.element.android.libraries.matrix.test.A_ROOM_NAME
3637
import io.element.android.libraries.matrix.test.A_SERVER_LIST
38+
import io.element.android.libraries.matrix.test.A_USER_ID
39+
import io.element.android.libraries.matrix.test.A_USER_ID_2
3740
import io.element.android.libraries.matrix.test.FakeMatrixClient
3841
import io.element.android.libraries.matrix.test.core.aBuildMeta
3942
import io.element.android.libraries.matrix.test.room.aRoomMember
43+
import io.element.android.libraries.matrix.test.room.aRoomPreview
4044
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
4145
import io.element.android.libraries.matrix.test.room.aRoomSummary
4246
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
@@ -47,6 +51,7 @@ import io.element.android.tests.testutils.lambda.assert
4751
import io.element.android.tests.testutils.lambda.lambdaRecorder
4852
import io.element.android.tests.testutils.lambda.value
4953
import io.element.android.tests.testutils.test
54+
import kotlinx.coroutines.ExperimentalCoroutinesApi
5055
import kotlinx.coroutines.flow.flowOf
5156
import kotlinx.coroutines.test.runTest
5257
import org.junit.Rule
@@ -253,10 +258,10 @@ class JoinRoomPresenterTest {
253258
}
254259
}
255260

261+
@OptIn(ExperimentalCoroutinesApi::class)
256262
@Test
257263
fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest {
258264
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
259-
val matrixClient = FakeMatrixClient().apply {
260265
val matrixClient = FakeMatrixClient(
261266
getRoomPreviewResult = { _, _ ->
262267
Result.success(
@@ -266,6 +271,14 @@ class JoinRoomPresenterTest {
266271
joinRule = JoinRule.Public,
267272
currentUserMembership = CurrentUserMembership.BANNED,
268273
),
274+
roomMembershipDetails = {
275+
Result.success(
276+
RoomMembershipDetails(
277+
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
278+
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
279+
)
280+
)
281+
}
269282
)
270283
)
271284
}
@@ -278,7 +291,13 @@ class JoinRoomPresenterTest {
278291
matrixClient = matrixClient
279292
)
280293
presenter.test {
294+
// Skip initial state
295+
skipItems(1)
296+
297+
// Advance until the room info is loaded and the presenter recomposes. The room preview info still needs to be loaded async.
281298
skipItems(1)
299+
300+
// Now we should have the room info
282301
awaitItem().also { state ->
283302
assertThat(state.joinAuthorisationStatus).isInstanceOf(JoinAuthorisationStatus.IsBanned::class.java)
284303
}

features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ class JoinRoomViewTest {
213213
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
214214
rule.setJoinRoomView(
215215
aJoinRoomState(
216-
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null)),
216+
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
217217
eventSink = eventsRecorder,
218218
),
219219
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.api.room
9+
10+
/**
11+
* Room membership details for the current user and the sender of the membership event.
12+
*
13+
* It also includes the reason the current user's membership changed, if any.
14+
*/
15+
data class RoomMembershipDetails(
16+
val currentUserMember: RoomMember,
17+
val senderMember: RoomMember?,
18+
) {
19+
val membershipChangeReason: String? = currentUserMember.membershipChangeReason
20+
}

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomPreview.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ interface RoomPreview : AutoCloseable {
2222
* Forget the room if we had access to it, and it was left or banned.
2323
*/
2424
suspend fun forget(): Result<Unit>
25+
26+
/**
27+
* Get the membership details of the user in the room, as well as from the user who sent the `m.room.member` event.
28+
*/
29+
suspend fun membershipDetails(): Result<RoomMembershipDetails?>
2530
}

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomPreview.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class RustRoomPreview(
4040
inner.forget()
4141
}
4242

43+
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = runCatching {
44+
val details = inner.ownMembershipDetails() ?: return@runCatching null
45+
RoomMembershipDetails(
46+
currentUserMember = RoomMemberMapper.map(details.ownRoomMember),
47+
senderMember = details.senderRoomMember?.let { RoomMemberMapper.map(it) },
48+
)
49+
}
50+
4351
override fun close() {
4452
inner.destroy()
4553
}

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomPreview.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class FakeRoomPreview(
2222
override val info: RoomPreviewInfo = aRoomPreviewInfo(),
2323
private val declineInviteResult: () -> Result<Unit> = { lambdaError() },
2424
private val forgetRoomResult: () -> Result<Unit> = { lambdaError() },
25+
private val roomMembershipDetails: () -> Result<RoomMembershipDetails?> = { lambdaError() },
2526
) : RoomPreview {
2627
override suspend fun leave(): Result<Unit> = simulateLongTask {
2728
declineInviteResult()
@@ -31,5 +32,9 @@ class FakeRoomPreview(
3132
forgetRoomResult()
3233
}
3334

35+
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = simulateLongTask {
36+
roomMembershipDetails()
37+
}
38+
3439
override fun close() = Unit
3540
}

0 commit comments

Comments
 (0)