Reload member list after moderation actions (#5268)
* Reload member list after moderation actions The previous `runActionAndWaitForMembershipChange` logic wasn't really doing anything, as the modified flow was never used. * Make sure we always set the value in the member list state flow, even if the underlying coroutine scope is no longer there. With `emit`, the `Ready` state was not emitted if the member list was loaded way too fast.
This commit is contained in:
parent
25da1cba83
commit
2eacce47c0
6 changed files with 61 additions and 23 deletions
|
|
@ -38,6 +38,7 @@ import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentMap
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -66,11 +67,6 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val canInvite by room.canInviteAsState(syncUpdateFlow.value)
|
val canInvite by room.canInviteAsState(syncUpdateFlow.value)
|
||||||
val roomModerationState = roomMembersModerationPresenter.present()
|
val roomModerationState = roomMembersModerationPresenter.present()
|
||||||
val activeRoomMemberCount by produceState(0L) {
|
|
||||||
room.roomInfoFlow.map { it.activeMembersCount }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collect { value = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
val roomMemberIdentityStates by produceState(persistentMapOf<UserId, IdentityState>()) {
|
val roomMemberIdentityStates by produceState(persistentMapOf<UserId, IdentityState>()) {
|
||||||
room.roomMemberIdentityStateChange(waitForEncryption = true)
|
room.roomMemberIdentityStateChange(waitForEncryption = true)
|
||||||
|
|
@ -81,8 +77,12 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the room members when the screen is loaded or the active member count changes
|
// Update the room members when the screen is loaded or the active member count changes
|
||||||
LaunchedEffect(activeRoomMemberCount) {
|
LaunchedEffect(Unit) {
|
||||||
room.updateMembers()
|
room.roomInfoFlow.map { it.activeMembersCount }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collectLatest {
|
||||||
|
room.updateMembers()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(membersState, roomMemberIdentityStates) {
|
LaunchedEffect(membersState, roomMemberIdentityStates) {
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,6 @@ class RoomMemberListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the update to be processed
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
// Update the room members state as `Room.updateMembers()` would have done with the actual implementation
|
// Update the room members state as `Room.updateMembers()` would have done with the actual implementation
|
||||||
room.givenRoomMembersState(RoomMembersState.Ready(persistentListOf()))
|
room.givenRoomMembersState(RoomMembersState.Ready(persistentListOf()))
|
||||||
// Wait for another update
|
// Wait for another update
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,12 @@ import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class RoomMemberModerationPresenter @Inject constructor(
|
class RoomMemberModerationPresenter @Inject constructor(
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
|
|
@ -82,6 +84,9 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RoomMemberModerationEvents.ProcessAction -> {
|
is RoomMemberModerationEvents.ProcessAction -> {
|
||||||
|
// First, hide any list of existing actions that could be displayed
|
||||||
|
moderationActions.value = persistentListOf()
|
||||||
|
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
is ModerationAction.DisplayProfile -> Unit
|
is ModerationAction.DisplayProfile -> Unit
|
||||||
is ModerationAction.KickUser -> {
|
is ModerationAction.KickUser -> {
|
||||||
|
|
@ -118,6 +123,7 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
is InternalRoomMemberModerationEvents.Reset -> {
|
is InternalRoomMemberModerationEvents.Reset -> {
|
||||||
selectedUser = null
|
selectedUser = null
|
||||||
|
moderationActions.value = persistentListOf()
|
||||||
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
||||||
banUserAsyncAction.value = AsyncAction.Uninitialized
|
banUserAsyncAction.value = AsyncAction.Uninitialized
|
||||||
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
||||||
|
|
@ -204,7 +210,15 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||||
action.runUpdatingState {
|
action.runUpdatingState {
|
||||||
val result = block()
|
val result = block()
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
room.membersStateFlow.drop(1).take(1)
|
// We wait a bit to ensure the server has processed the membership change
|
||||||
|
delay(50.milliseconds)
|
||||||
|
|
||||||
|
// Update the members to ensure we have the latest state
|
||||||
|
launch { room.updateMembers() }
|
||||||
|
|
||||||
|
// Wait for the membership change to be processed and returned
|
||||||
|
// We drop the first emission as it's the current state
|
||||||
|
room.membersStateFlow.drop(1).first()
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -217,7 +218,14 @@ class RoomMemberModerationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - do kick user with success`() = runTest {
|
fun `present - do kick user with success`() = runTest {
|
||||||
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
val room = aJoinedRoom()
|
||||||
|
room.baseRoom.givenUpdateMembersResult {
|
||||||
|
// Simulate the member list being updated
|
||||||
|
room.givenRoomMembersState(RoomMembersState.Ready(
|
||||||
|
persistentListOf(aRoomMember())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
val initialState = awaitState()
|
val initialState = awaitState()
|
||||||
initialState.eventSink(
|
initialState.eventSink(
|
||||||
RoomMemberModerationEvents.ProcessAction(
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
|
@ -238,7 +246,14 @@ class RoomMemberModerationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - do ban user with success`() = runTest {
|
fun `present - do ban user with success`() = runTest {
|
||||||
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
val room = aJoinedRoom()
|
||||||
|
room.baseRoom.givenUpdateMembersResult {
|
||||||
|
// Simulate the member list being updated
|
||||||
|
room.givenRoomMembersState(RoomMembersState.Ready(
|
||||||
|
persistentListOf(aRoomMember())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
val initialState = awaitState()
|
val initialState = awaitState()
|
||||||
initialState.eventSink(
|
initialState.eventSink(
|
||||||
RoomMemberModerationEvents.ProcessAction(
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
|
@ -259,7 +274,14 @@ class RoomMemberModerationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - do unban user with success`() = runTest {
|
fun `present - do unban user with success`() = runTest {
|
||||||
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
val room = aJoinedRoom()
|
||||||
|
room.baseRoom.givenUpdateMembersResult {
|
||||||
|
// Simulate the member list being updated
|
||||||
|
room.givenRoomMembersState(RoomMembersState.Ready(
|
||||||
|
persistentListOf(aRoomMember())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
val initialState = awaitState()
|
val initialState = awaitState()
|
||||||
initialState.eventSink(
|
initialState.eventSink(
|
||||||
RoomMemberModerationEvents.ProcessAction(
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
|
@ -326,7 +348,7 @@ class RoomMemberModerationPresenterTest {
|
||||||
banUserResult: Result<Unit> = Result.success(Unit),
|
banUserResult: Result<Unit> = Result.success(Unit),
|
||||||
unBanUserResult: Result<Unit> = Result.success(Unit),
|
unBanUserResult: Result<Unit> = Result.success(Unit),
|
||||||
targetRoomMember: RoomMember? = null,
|
targetRoomMember: RoomMember? = null,
|
||||||
): JoinedRoom {
|
): FakeJoinedRoom {
|
||||||
return FakeJoinedRoom(
|
return FakeJoinedRoom(
|
||||||
kickUserResult = { _, _ -> kickUserResult },
|
kickUserResult = { _, _ -> kickUserResult },
|
||||||
banUserResult = { _, _ -> banUserResult },
|
banUserResult = { _, _ -> banUserResult },
|
||||||
|
|
@ -335,6 +357,7 @@ class RoomMemberModerationPresenterTest {
|
||||||
canBanResult = { _ -> Result.success(canBan) },
|
canBanResult = { _ -> Result.success(canBan) },
|
||||||
canKickResult = { _ -> Result.success(canKick) },
|
canKickResult = { _ -> Result.success(canKick) },
|
||||||
userRoleResult = { Result.success(myUserRole) },
|
userRoleResult = { Result.success(myUserRole) },
|
||||||
|
updateMembersResult = { Result.success(Unit) }
|
||||||
),
|
),
|
||||||
).apply {
|
).apply {
|
||||||
val roomMembers = listOfNotNull(targetRoomMember).toPersistentList()
|
val roomMembers = listOfNotNull(targetRoomMember).toPersistentList()
|
||||||
|
|
|
||||||
|
|
@ -79,35 +79,35 @@ internal class RoomMemberListFetcher(
|
||||||
Timber.i("Loading cached members for room $roomId")
|
Timber.i("Loading cached members for room $roomId")
|
||||||
try {
|
try {
|
||||||
// Send current member list with pending state to notify the UI that we are loading new members
|
// Send current member list with pending state to notify the UI that we are loading new members
|
||||||
emit(pendingWithCurrentMembers())
|
value = pendingWithCurrentMembers()
|
||||||
val members = parseAndEmitMembers(room.membersNoSync())
|
val members = parseAndEmitMembers(room.membersNoSync())
|
||||||
val newState = if (asPendingState) {
|
val newState = if (asPendingState) {
|
||||||
RoomMembersState.Pending(prevRoomMembers = members)
|
RoomMembersState.Pending(prevRoomMembers = members)
|
||||||
} else {
|
} else {
|
||||||
RoomMembersState.Ready(members)
|
RoomMembersState.Ready(members)
|
||||||
}
|
}
|
||||||
emit(newState)
|
value = newState
|
||||||
} catch (exception: CancellationException) {
|
} catch (exception: CancellationException) {
|
||||||
Timber.d("Cancelled loading cached members for room $roomId")
|
Timber.d("Cancelled loading cached members for room $roomId")
|
||||||
throw exception
|
throw exception
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Timber.e(exception, "Failed to load cached members for room $roomId")
|
Timber.e(exception, "Failed to load cached members for room $roomId")
|
||||||
emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
|
value = RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun MutableStateFlow<RoomMembersState>.fetchRemoteRoomMembers() {
|
private suspend fun MutableStateFlow<RoomMembersState>.fetchRemoteRoomMembers() {
|
||||||
try {
|
try {
|
||||||
// Send current member list with pending state to notify the UI that we are loading new members
|
// Send current member list with pending state to notify the UI that we are loading new members
|
||||||
emit(pendingWithCurrentMembers())
|
value = pendingWithCurrentMembers()
|
||||||
// Start loading new members
|
// Start loading new members
|
||||||
emit(RoomMembersState.Ready(parseAndEmitMembers(room.members())))
|
value = RoomMembersState.Ready(parseAndEmitMembers(room.members()))
|
||||||
} catch (exception: CancellationException) {
|
} catch (exception: CancellationException) {
|
||||||
Timber.d("Cancelled loading updated members for room $roomId")
|
Timber.d("Cancelled loading updated members for room $roomId")
|
||||||
throw exception
|
throw exception
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Timber.e(exception, "Failed to load updated members for room $roomId")
|
Timber.e(exception, "Failed to load updated members for room $roomId")
|
||||||
emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
|
value = RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class FakeBaseRoom(
|
||||||
private val markAsReadResult: (ReceiptType) -> Result<Unit> = { Result.success(Unit) },
|
private val markAsReadResult: (ReceiptType) -> Result<Unit> = { Result.success(Unit) },
|
||||||
private val powerLevelsResult: () -> Result<RoomPowerLevelsValues> = { lambdaError() },
|
private val powerLevelsResult: () -> Result<RoomPowerLevelsValues> = { lambdaError() },
|
||||||
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
|
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
private val updateMembersResult: () -> Unit = { lambdaError() },
|
private var updateMembersResult: () -> Unit = { lambdaError() },
|
||||||
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
|
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
|
||||||
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
|
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
|
||||||
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
||||||
|
|
@ -223,6 +223,10 @@ class FakeBaseRoom(
|
||||||
override suspend fun reportRoom(reason: String?) = reportRoomResult(reason)
|
override suspend fun reportRoom(reason: String?) = reportRoomResult(reason)
|
||||||
|
|
||||||
override fun predecessorRoom(): PredecessorRoom? = predecessorRoomResult()
|
override fun predecessorRoom(): PredecessorRoom? = predecessorRoomResult()
|
||||||
|
|
||||||
|
fun givenUpdateMembersResult(result: () -> Unit) {
|
||||||
|
updateMembersResult = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues(
|
fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue