Room member moderation: kick, ban and unban (#2496)

* Room member moderation: kick, ban and unban

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-03-06 16:44:05 +01:00 committed by GitHub
parent 47539479dd
commit 134cacb024
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 1410 additions and 83 deletions

View file

@ -79,9 +79,9 @@ sealed interface AsyncAction<out T> {
fun isUninitialized(): Boolean = this == Uninitialized
fun isConfirming(): Boolean = this is Confirming
fun isConfirming(): Boolean = this == Confirming
fun isLoading(): Boolean = this is Loading
fun isLoading(): Boolean = this == Loading
fun isFailure(): Boolean = this is Failure

View file

@ -47,3 +47,9 @@ inline fun <R, T> Result<T>.flatMapCatching(transform: (T) -> Result<R>): Result
onFailure = { Result.failure(it) }
)
}
inline fun <T> Result<T>.finally(block: (exception: Throwable?) -> Unit): Result<T> {
onSuccess { block(null) }
onFailure(block)
return this
}

View file

@ -47,6 +47,7 @@ enum class AvatarSize(val dp: Dp) {
InviteSender(16.dp),
EditRoomDetails(70.dp),
RoomListManageUser(70.dp),
NotificationsOptIn(32.dp),

View file

@ -193,7 +193,7 @@ class AsyncIndicatorTests {
currentAnimationState = TransitionStateSnapshot(transitionState),
)
}.test {
var firstItem: Any? = null
var firstItem: Any?
skipItems(1)
state.enqueue(composable = {})
state.enqueue(composable = {})

View file

@ -140,6 +140,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserInvite(userId: UserId): Result<Boolean>
suspend fun canUserKick(userId: UserId): Result<Boolean>
suspend fun canUserBan(userId: UserId): Result<Boolean>
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
@ -177,6 +179,12 @@ interface MatrixRoom : Closeable {
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
suspend fun kickUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun banUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun unbanUser(userId: UserId, reason: String? = null): Result<Unit>
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
/**

View file

@ -25,6 +25,11 @@ import io.element.android.libraries.matrix.api.room.StateEventType
*/
suspend fun MatrixRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
/**
* Shortcut for calling [MatrixRoom.canUserKick] with our own user.
*/
suspend fun MatrixRoom.canKick(): Result<Boolean> = canUserKick(sessionId)
/**
* Shortcut for calling [MatrixRoom.canBanUser] with our own user.
*/

View file

@ -39,6 +39,7 @@ interface MatrixTimeline : AutoCloseable {
val paginationState: StateFlow<PaginationState>
val timelineItems: Flow<List<MatrixTimelineItem>>
val membershipChangeEventReceived: Flow<Unit>
suspend fun paginateBackwards(requestSize: Int): Result<Unit>
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>

View file

@ -70,6 +70,8 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -159,6 +161,12 @@ class RustMatrixRoom(
override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow()
init {
timeline.membershipChangeEventReceived
.onEach { roomMemberListFetcher.fetchRoomMembers() }
.launchIn(roomCoroutineScope)
}
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
@ -340,6 +348,12 @@ class RustMatrixRoom(
}
}
override suspend fun canUserKick(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserKick(userId.value)
}
}
override suspend fun canUserBan(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserBan(userId.value)
@ -469,6 +483,24 @@ class RustMatrixRoom(
}
}
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.kickUser(userId.value, reason)
}
}
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.banUser(userId.value, reason)
}
}
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.unbanUser(userId.value, reason)
}
}
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.setIsFavourite(isFavorite, null)

View file

@ -65,7 +65,7 @@ internal class RoomMemberListFetcher(
if (_membersFlow.value !is MatrixRoomMembersState.Ready) {
fetchCachedRoomMembers()
} else {
Timber.i("No need to load cached members found for room $roomId")
Timber.i("Cached members not found for $roomId")
}
}

View file

@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
@ -55,6 +56,8 @@ class AsyncMatrixTimeline(
}
private val closeSignal = CompletableDeferred<Unit>()
override val membershipChangeEventReceived = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
init {
coroutineScope.launch {
val delegateTimeline = timeline.await()
@ -64,6 +67,9 @@ class AsyncMatrixTimeline(
delegateTimeline.paginationState
.onEach { _paginationState.value = it }
.launchIn(this)
delegateTimeline.membershipChangeEventReceived
.onEach { membershipChangeEventReceived.emit(it) }
.launchIn(this)
launch {
withContext(NonCancellable) {

View file

@ -17,6 +17,9 @@
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -31,6 +34,9 @@ internal class MatrixTimelineDiffProcessor(
) {
private val mutex = Mutex()
private val _membershipChangeEventReceived = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val membershipChangeEventReceived: Flow<Unit> = _membershipChangeEventReceived
suspend fun postItems(items: List<TimelineItem>) {
updateTimelineItems {
Timber.v("Update timeline items from postItems (with ${items.size} items) on ${Thread.currentThread()}")
@ -63,6 +69,11 @@ internal class MatrixTimelineDiffProcessor(
}
TimelineChange.PUSH_BACK -> {
val item = diff.pushBack()?.asMatrixTimelineItem() ?: return
if (item is MatrixTimelineItem.Event && item.event.content is RoomMembershipContent) {
// TODO - This is a temporary solution to notify the room screen about membership changes
// Ideally, this should be implemented by the Rust SDK
_membershipChangeEventReceived.tryEmit(Unit)
}
add(item)
}
TimelineChange.PUSH_FRONT -> {

View file

@ -114,6 +114,8 @@ class RustMatrixTimeline(
)
}
override val membershipChangeEventReceived: Flow<Unit> = timelineDiffProcessor.membershipChangeEventReceived
init {
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")

View file

@ -95,6 +95,7 @@ class FakeMatrixRoom(
private var joinRoomResult = Result.success(Unit)
private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true)
private var canKickResult = Result.success(false)
private var canBanResult = Result.success(false)
private var canRedactOwnResult = Result.success(canRedactOwn)
private var canRedactOtherResult = Result.success(canRedactOther)
@ -111,6 +112,9 @@ class FakeMatrixRoom(
private var cancelSendResult = Result.success(Unit)
private var forwardEventResult = Result.success(Unit)
private var reportContentResult = Result.success(Unit)
private var kickUserResult = Result.success(Unit)
private var banUserResult = Result.success(Unit)
private var unBanUserResult = Result.success(Unit)
private var sendLocationResult = Result.success(Unit)
private var createPollResult = Result.success(Unit)
private var editPollResult = Result.success(Unit)
@ -299,6 +303,10 @@ class FakeMatrixRoom(
return canBanResult
}
override suspend fun canUserKick(userId: UserId): Result<Boolean> {
return canKickResult
}
override suspend fun canUserInvite(userId: UserId): Result<Boolean> {
return canInviteResult
}
@ -398,6 +406,18 @@ class FakeMatrixRoom(
return reportContentResult
}
override suspend fun kickUser(userId: UserId, reason: String?): Result<Unit> {
return kickUserResult
}
override suspend fun banUser(userId: UserId, reason: String?): Result<Unit> {
return banUserResult
}
override suspend fun unbanUser(userId: UserId, reason: String?): Result<Unit> {
return unBanUserResult
}
val setIsFavoriteCalls = mutableListOf<Boolean>()
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> {
@ -522,6 +542,10 @@ class FakeMatrixRoom(
joinRoomResult = result
}
fun givenCanKickResult(result: Result<Boolean>) {
canKickResult = result
}
fun givenCanBanResult(result: Result<Boolean>) {
canBanResult = result
}
@ -598,6 +622,18 @@ class FakeMatrixRoom(
reportContentResult = result
}
fun givenKickUserResult(result: Result<Unit>) {
kickUserResult = result
}
fun givenBanUserResult(result: Result<Unit>) {
banUserResult = result
}
fun givenUnbanUserResult(result: Result<Unit>) {
unBanUserResult = result
}
fun givenSendLocationResult(result: Result<Unit>) {
sendLocationResult = result
}

View file

@ -24,6 +24,7 @@ import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
@ -59,6 +60,8 @@ class FakeMatrixTimeline(
override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards()
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards()
override val membershipChangeEventReceived = MutableSharedFlow<Unit>()
private suspend fun paginateBackwards(): Result<Unit> {
updatePaginationState {
copy(isBackPaginating = true)
@ -73,6 +76,10 @@ class FakeMatrixTimeline(
return Result.success(Unit)
}
fun givenMembershipChangeEventReceived() {
membershipChangeEventReceived.tryEmit(Unit)
}
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = simulateLongTask {
Result.success(Unit)
}

View file

@ -114,6 +114,7 @@
<string name="common_audio">"Audio"</string>
<string name="common_blocked_users">"Blocked users"</string>
<string name="common_bubbles">"Bubbles"</string>
<string name="common_call_invite">"Call in progress (unsupported)"</string>
<string name="common_chat_backup">"Chat backup"</string>
<string name="common_copyright">"Copyright"</string>
<string name="common_creating_room">"Creating room…"</string>