Observe ignoredUsersFlow to have live data about blocked user.

This will also ensure that blocking a user will work even if the user is not a member of the room (preparatory work for user permalink)
This commit is contained in:
Benoit Marty 2024-04-17 13:21:11 +02:00 committed by Benoit Marty
parent f2f29c6638
commit 87298803c6
3 changed files with 46 additions and 35 deletions

View file

@ -39,6 +39,10 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RoomMemberDetailsPresenter @AssistedInject constructor( class RoomMemberDetailsPresenter @AssistedInject constructor(
@ -57,14 +61,13 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) } var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val roomMember by room.getRoomMemberAsState(roomMemberId) val roomMember by room.getRoomMemberAsState(roomMemberId)
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
// the room member is not really live... val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val isBlocked: MutableState<AsyncData<Boolean>> = remember(roomMember) { LaunchedEffect(Unit) {
val isIgnored = roomMember?.isIgnored client.ignoredUsersFlow
if (isIgnored == null) { .map { ignoredUsers -> roomMemberId in ignoredUsers }
mutableStateOf(AsyncData.Uninitialized) .distinctUntilChanged()
} else { .onEach { isBlocked.value = AsyncData.Success(it) }
mutableStateOf(AsyncData.Success(isIgnored)) .launchIn(this)
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Update room member info when opening this screen // Update room member info when opening this screen
@ -132,28 +135,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch { private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
isBlockedState.value = AsyncData.Loading(false) isBlockedState.value = AsyncData.Loading(false)
client.ignoreUser(userId) client.ignoreUser(userId)
.fold( .onFailure {
onSuccess = { isBlockedState.value = AsyncData.Failure(it, false)
isBlockedState.value = AsyncData.Success(true) }
room.getUpdatedMember(userId) // Note: on success, ignoredUserList will be updated.
},
onFailure = {
isBlockedState.value = AsyncData.Failure(it, false)
}
)
} }
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch { private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
isBlockedState.value = AsyncData.Loading(true) isBlockedState.value = AsyncData.Loading(true)
client.unignoreUser(userId) client.unignoreUser(userId)
.fold( .onFailure {
onSuccess = { isBlockedState.value = AsyncData.Failure(it, true)
isBlockedState.value = AsyncData.Success(false) }
room.getUpdatedMember(userId) // Note: on success, ignoredUserList will be updated.
},
onFailure = {
isBlockedState.value = AsyncData.Failure(it, true)
}
)
} }
} }

View file

@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.members.details
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.api.StartDMAction
@ -63,7 +64,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(roomMember.userId.value) assertThat(initialState.userId).isEqualTo(roomMember.userId.value)
assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.userName).isEqualTo(roomMember.displayName)
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
@ -90,7 +91,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.userName).isEqualTo(roomMember.displayName)
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
@ -113,7 +114,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.userName).isEqualTo(roomMember.displayName)
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
@ -127,7 +128,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true))
val dialogState = awaitItem() val dialogState = awaitItem()
@ -142,17 +143,24 @@ class RoomMemberDetailsPresenterTests {
@Test @Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val presenter = createRoomMemberDetailsPresenter() val client = FakeMatrixClient()
val roomMember = aRoomMember()
val presenter = createRoomMemberDetailsPresenter(
client = client,
roomMember = roomMember,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(roomMember.userId))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
} }
} }
@ -165,7 +173,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem() val errorState = awaitItem()
@ -182,7 +190,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true))
val dialogState = awaitItem() val dialogState = awaitItem()
@ -202,7 +210,7 @@ class RoomMemberDetailsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitFirstItem()
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE) val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
@ -229,6 +237,11 @@ class RoomMemberDetailsPresenterTests {
} }
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
private fun createRoomMemberDetailsPresenter( private fun createRoomMemberDetailsPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
room: MatrixRoom = aMatrixRoom(), room: MatrixRoom = aMatrixRoom(),

View file

@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -205,6 +206,10 @@ class FakeMatrixClient(
return RoomMembershipObserver() return RoomMembershipObserver()
} }
suspend fun emitIgnoreUserList(users: List<UserId>) {
ignoredUsersFlow.emit(users.toImmutableList())
}
// Mocks // Mocks
fun givenLogoutError(failure: Throwable?) { fun givenLogoutError(failure: Throwable?) {