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.
This commit is contained in:
parent
311fbb84f7
commit
02addf54dd
11 changed files with 101 additions and 17 deletions
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.exception.ErrorKind
|
||||||
import io.element.android.libraries.matrix.api.getRoomInfoFlow
|
import io.element.android.libraries.matrix.api.getRoomInfoFlow
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
|
|
@ -97,7 +98,20 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
when {
|
when {
|
||||||
isDismissingContent -> value = ContentState.Dismissing
|
isDismissingContent -> value = ContentState.Dismissing
|
||||||
roomInfo.isPresent -> {
|
roomInfo.isPresent -> {
|
||||||
value = roomInfo.get().toContentState()
|
val (sender, reason) = when (roomInfo.get().currentUserMembership) {
|
||||||
|
CurrentUserMembership.BANNED -> {
|
||||||
|
// Workaround to get info about the sender for banned rooms
|
||||||
|
// TODO re-do this once we have a better API in the SDK
|
||||||
|
val preview = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
|
||||||
|
val membershipDetalis = preview.getOrNull()?.membershipDetails()?.getOrNull()
|
||||||
|
membershipDetalis?.senderMember to membershipDetalis?.currentUserMember?.membershipChangeReason
|
||||||
|
}
|
||||||
|
CurrentUserMembership.INVITED -> {
|
||||||
|
roomInfo.get().inviter to null
|
||||||
|
}
|
||||||
|
else -> null to null
|
||||||
|
}
|
||||||
|
value = roomInfo.get().toContentState(sender, reason)
|
||||||
}
|
}
|
||||||
roomDescription.isPresent -> {
|
roomDescription.isPresent -> {
|
||||||
value = roomDescription.get().toContentState()
|
value = roomDescription.get().toContentState()
|
||||||
|
|
@ -106,10 +120,19 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
value = ContentState.Loading
|
value = ContentState.Loading
|
||||||
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
|
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
|
||||||
value = result.fold(
|
value = result.fold(
|
||||||
onSuccess = { previewInfo ->
|
|
||||||
previewInfo.toContentState()
|
|
||||||
onSuccess = { preview ->
|
onSuccess = { preview ->
|
||||||
preview.info.toContentState()
|
val membershipInfo = when (preview.info.membership) {
|
||||||
|
CurrentUserMembership.INVITED,
|
||||||
|
CurrentUserMembership.BANNED,
|
||||||
|
CurrentUserMembership.KNOCKED -> {
|
||||||
|
preview.membershipDetails().getOrNull()
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
preview.info.toContentState(
|
||||||
|
senderMember = membershipInfo?.senderMember,
|
||||||
|
reason = membershipInfo?.currentUserMember?.membershipChangeReason,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onFailure = { throwable ->
|
onFailure = { throwable ->
|
||||||
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
|
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
|
||||||
|
|
@ -213,7 +236,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomPreviewInfo.toContentState(): ContentState {
|
private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: String?): ContentState {
|
||||||
return ContentState.Loaded(
|
return ContentState.Loaded(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -224,8 +247,8 @@ private fun RoomPreviewInfo.toContentState(): ContentState {
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (membership) {
|
joinAuthorisationStatus = when (membership) {
|
||||||
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(null)
|
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(senderMember?.toInviteSender())
|
||||||
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(null)
|
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
|
||||||
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||||
else -> joinRule.toJoinAuthorisationStatus()
|
else -> joinRule.toJoinAuthorisationStatus()
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +275,7 @@ internal fun RoomDescription.toContentState(): ContentState {
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun MatrixRoomInfo.toContentState(): ContentState {
|
internal fun MatrixRoomInfo.toContentState(membershipSender: RoomMember?, reason: String?): ContentState {
|
||||||
return ContentState.Loaded(
|
return ContentState.Loaded(
|
||||||
roomId = id,
|
roomId = id,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -264,10 +287,11 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (currentUserMembership) {
|
joinAuthorisationStatus = when (currentUserMembership) {
|
||||||
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||||
inviteSender = inviter?.toInviteSender()
|
inviteSender = membershipSender?.toInviteSender()
|
||||||
)
|
)
|
||||||
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
|
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
|
||||||
banSender = inviter?.toInviteSender()
|
banSender = membershipSender?.toInviteSender(),
|
||||||
|
reason = reason,
|
||||||
)
|
)
|
||||||
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||||
else -> joinRule.toJoinAuthorisationStatus()
|
else -> joinRule.toJoinAuthorisationStatus()
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ sealed interface JoinAuthorisationStatus {
|
||||||
data object None : JoinAuthorisationStatus
|
data object None : JoinAuthorisationStatus
|
||||||
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
|
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
|
||||||
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||||
data class IsBanned(val banSender: InviteSender?) : JoinAuthorisationStatus
|
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
|
||||||
data object IsKnocked : JoinAuthorisationStatus
|
data object IsKnocked : JoinAuthorisationStatus
|
||||||
data object CanKnock : JoinAuthorisationStatus
|
data object CanKnock : JoinAuthorisationStatus
|
||||||
data object CanJoin : JoinAuthorisationStatus
|
data object CanJoin : JoinAuthorisationStatus
|
||||||
|
|
|
||||||
|
|
@ -115,12 +115,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
contentState = aLoadedContentState(
|
contentState = aLoadedContentState(
|
||||||
name = "A banned room",
|
name = "A banned room",
|
||||||
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
|
||||||
InviteSender(
|
banSender = InviteSender(
|
||||||
userId = UserId("@alice:domain"),
|
userId = UserId("@alice:domain"),
|
||||||
displayName = "Alice",
|
displayName = "Alice",
|
||||||
avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender),
|
avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender),
|
||||||
membershipChangeReason = "spamming"
|
membershipChangeReason = "spamming"
|
||||||
)
|
),
|
||||||
|
reason = "spamming",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -287,8 +287,8 @@ private fun JoinBannedFooter(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
val banReason = status.banSender?.membershipChangeReason?.let {
|
val banReason = status.reason?.let {
|
||||||
stringResource(R.string.screen_join_room_ban_reason, it)
|
stringResource(R.string.screen_join_room_ban_reason, it.removeSuffix("."))
|
||||||
}
|
}
|
||||||
val title = if (status.banSender != null) {
|
val title = if (status.banSender != null) {
|
||||||
stringResource(R.string.screen_join_room_ban_by_message, status.banSender.displayName)
|
stringResource(R.string.screen_join_room_ban_by_message, status.banSender.displayName)
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,19 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||||
import io.element.android.libraries.matrix.api.exception.ErrorKind
|
import io.element.android.libraries.matrix.api.exception.ErrorKind
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SERVER_LIST
|
import io.element.android.libraries.matrix.test.A_SERVER_LIST
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomPreview
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
|
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
|
|
@ -47,6 +51,7 @@ import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -253,10 +258,10 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest {
|
fun `present - when room is banned, then join authorization is equal to IsBanned`() = runTest {
|
||||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
|
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.BANNED, joinRule = JoinRule.Public)
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
getRoomPreviewResult = { _, _ ->
|
getRoomPreviewResult = { _, _ ->
|
||||||
Result.success(
|
Result.success(
|
||||||
|
|
@ -266,6 +271,14 @@ class JoinRoomPresenterTest {
|
||||||
joinRule = JoinRule.Public,
|
joinRule = JoinRule.Public,
|
||||||
currentUserMembership = CurrentUserMembership.BANNED,
|
currentUserMembership = CurrentUserMembership.BANNED,
|
||||||
),
|
),
|
||||||
|
roomMembershipDetails = {
|
||||||
|
Result.success(
|
||||||
|
RoomMembershipDetails(
|
||||||
|
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
|
||||||
|
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +291,13 @@ class JoinRoomPresenterTest {
|
||||||
matrixClient = matrixClient
|
matrixClient = matrixClient
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
// Skip initial state
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
|
// Advance until the room info is loaded and the presenter recomposes. The room preview info still needs to be loaded async.
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
// Now we should have the room info
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAuthorisationStatus).isInstanceOf(JoinAuthorisationStatus.IsBanned::class.java)
|
assertThat(state.joinAuthorisationStatus).isInstanceOf(JoinAuthorisationStatus.IsBanned::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ class JoinRoomViewTest {
|
||||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
rule.setJoinRoomView(
|
rule.setJoinRoomView(
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null)),
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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.api.room
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room membership details for the current user and the sender of the membership event.
|
||||||
|
*
|
||||||
|
* It also includes the reason the current user's membership changed, if any.
|
||||||
|
*/
|
||||||
|
data class RoomMembershipDetails(
|
||||||
|
val currentUserMember: RoomMember,
|
||||||
|
val senderMember: RoomMember?,
|
||||||
|
) {
|
||||||
|
val membershipChangeReason: String? = currentUserMember.membershipChangeReason
|
||||||
|
}
|
||||||
|
|
@ -22,4 +22,9 @@ interface RoomPreview : AutoCloseable {
|
||||||
* Forget the room if we had access to it, and it was left or banned.
|
* Forget the room if we had access to it, and it was left or banned.
|
||||||
*/
|
*/
|
||||||
suspend fun forget(): Result<Unit>
|
suspend fun forget(): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the membership details of the user in the room, as well as from the user who sent the `m.room.member` event.
|
||||||
|
*/
|
||||||
|
suspend fun membershipDetails(): Result<RoomMembershipDetails?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,14 @@ class RustRoomPreview(
|
||||||
inner.forget()
|
inner.forget()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = runCatching {
|
||||||
|
val details = inner.ownMembershipDetails() ?: return@runCatching null
|
||||||
|
RoomMembershipDetails(
|
||||||
|
currentUserMember = RoomMemberMapper.map(details.ownRoomMember),
|
||||||
|
senderMember = details.senderRoomMember?.let { RoomMemberMapper.map(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
inner.destroy()
|
inner.destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class FakeRoomPreview(
|
||||||
override val info: RoomPreviewInfo = aRoomPreviewInfo(),
|
override val info: RoomPreviewInfo = aRoomPreviewInfo(),
|
||||||
private val declineInviteResult: () -> Result<Unit> = { lambdaError() },
|
private val declineInviteResult: () -> Result<Unit> = { lambdaError() },
|
||||||
private val forgetRoomResult: () -> Result<Unit> = { lambdaError() },
|
private val forgetRoomResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
private val roomMembershipDetails: () -> Result<RoomMembershipDetails?> = { lambdaError() },
|
||||||
) : RoomPreview {
|
) : RoomPreview {
|
||||||
override suspend fun leave(): Result<Unit> = simulateLongTask {
|
override suspend fun leave(): Result<Unit> = simulateLongTask {
|
||||||
declineInviteResult()
|
declineInviteResult()
|
||||||
|
|
@ -31,5 +32,9 @@ class FakeRoomPreview(
|
||||||
forgetRoomResult()
|
forgetRoomResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun membershipDetails(): Result<RoomMembershipDetails?> = simulateLongTask {
|
||||||
|
roomMembershipDetails()
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,13 @@ fun aRoomPreview(
|
||||||
info: RoomPreviewInfo = aRoomPreviewInfo(),
|
info: RoomPreviewInfo = aRoomPreviewInfo(),
|
||||||
declineInviteResult: () -> Result<Unit> = { lambdaError() },
|
declineInviteResult: () -> Result<Unit> = { lambdaError() },
|
||||||
forgetRoomResult: () -> Result<Unit> = { lambdaError() },
|
forgetRoomResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
roomMembershipDetails: () -> Result<RoomMembershipDetails?> = { lambdaError() },
|
||||||
) = FakeRoomPreview(
|
) = FakeRoomPreview(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
info = info,
|
info = info,
|
||||||
declineInviteResult = declineInviteResult,
|
declineInviteResult = declineInviteResult,
|
||||||
forgetRoomResult = forgetRoomResult,
|
forgetRoomResult = forgetRoomResult,
|
||||||
|
roomMembershipDetails = roomMembershipDetails,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomPreviewInfo(
|
fun aRoomPreviewInfo(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue