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:
parent
47539479dd
commit
134cacb024
113 changed files with 1410 additions and 83 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
InviteSender(16.dp),
|
||||
|
||||
EditRoomDetails(70.dp),
|
||||
RoomListManageUser(70.dp),
|
||||
|
||||
NotificationsOptIn(32.dp),
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ class RustMatrixTimeline(
|
|||
)
|
||||
}
|
||||
|
||||
override val membershipChangeEventReceived: Flow<Unit> = timelineDiffProcessor.membershipChangeEventReceived
|
||||
|
||||
init {
|
||||
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue