Let RoomMemberDetailsPresenter use UserProfilePresenter to reduce code duplication.
This commit is contained in:
committed by
Benoit Marty
parent
9b9fef1aa8
commit
9b82c6df1c
@@ -9,7 +9,7 @@ package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@@ -10,10 +10,9 @@ package io.element.android.features.roomdetails.impl.di
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.features.userprofile.api.UserProfileStatePresenterFactory
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
||||
@@ -22,13 +21,12 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
object RoomMemberModule {
|
||||
@Provides
|
||||
fun provideRoomMemberDetailsPresenterFactory(
|
||||
matrixClient: MatrixClient,
|
||||
room: MatrixRoom,
|
||||
startDMAction: StartDMAction,
|
||||
userProfileStatePresenterFactory: UserProfileStatePresenterFactory,
|
||||
): RoomMemberDetailsPresenter.Factory {
|
||||
return object : RoomMemberDetailsPresenter.Factory {
|
||||
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
||||
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction)
|
||||
return RoomMemberDetailsPresenter(roomMemberId, room, userProfileStatePresenterFactory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,153 +9,60 @@ package io.element.android.features.roomdetails.impl.members.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfilePresenterHelper
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.api.UserProfileStatePresenterFactory
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Presenter for room member details screen.
|
||||
* Rely on UserProfileStatePresenter, but override some fields with room member info when available.
|
||||
*/
|
||||
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
@Assisted private val roomMemberId: UserId,
|
||||
private val client: MatrixClient,
|
||||
private val room: MatrixRoom,
|
||||
private val startDMAction: StartDMAction,
|
||||
userProfileStatePresenterFactory: UserProfileStatePresenterFactory,
|
||||
) : Presenter<UserProfileState> {
|
||||
interface Factory {
|
||||
fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
|
||||
}
|
||||
|
||||
private val userProfilePresenterHelper = UserProfilePresenterHelper(
|
||||
userId = roomMemberId,
|
||||
client = client,
|
||||
)
|
||||
private val userProfilePresenter = userProfileStatePresenterFactory.create(roomMemberId)
|
||||
|
||||
@Composable
|
||||
override fun present(): UserProfileState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||
val roomMember by room.getRoomMemberAsState(roomMemberId)
|
||||
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
|
||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
val isCurrentUser = remember { client.isMe(roomMemberId) }
|
||||
val dmRoomId by userProfilePresenterHelper.getDmRoomId()
|
||||
val canCall by userProfilePresenterHelper.getCanCall(dmRoomId)
|
||||
LaunchedEffect(Unit) {
|
||||
client.ignoredUsersFlow
|
||||
.map { ignoredUsers -> roomMemberId in ignoredUsers }
|
||||
.distinctUntilChanged()
|
||||
.onEach { isBlocked.value = AsyncData.Success(it) }
|
||||
.launchIn(this)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
// Update room member info when opening this screen
|
||||
// We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState`
|
||||
room.getUpdatedMember(roomMemberId)
|
||||
.onFailure {
|
||||
// Not a member of the room, try to get the user profile
|
||||
userProfile = client.getProfile(roomMemberId).getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: UserProfileEvents) {
|
||||
when (event) {
|
||||
is UserProfileEvents.BlockUser -> {
|
||||
if (event.needsConfirmation) {
|
||||
confirmationDialog = ConfirmationDialog.Block
|
||||
} else {
|
||||
confirmationDialog = null
|
||||
userProfilePresenterHelper.blockUser(coroutineScope, isBlocked)
|
||||
}
|
||||
}
|
||||
is UserProfileEvents.UnblockUser -> {
|
||||
if (event.needsConfirmation) {
|
||||
confirmationDialog = ConfirmationDialog.Unblock
|
||||
} else {
|
||||
confirmationDialog = null
|
||||
userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked)
|
||||
}
|
||||
}
|
||||
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
|
||||
UserProfileEvents.ClearBlockUserError -> {
|
||||
isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse())
|
||||
}
|
||||
UserProfileEvents.StartDM -> {
|
||||
coroutineScope.launch {
|
||||
startDMAction.execute(roomMemberId, startDmActionState)
|
||||
}
|
||||
}
|
||||
UserProfileEvents.ClearStartDMState -> {
|
||||
startDmActionState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val userName: String? by produceState(
|
||||
initialValue = roomMember?.displayName ?: userProfile?.displayName,
|
||||
val roomUserName: String? by produceState(
|
||||
initialValue = roomMember?.displayName,
|
||||
key1 = roomMember,
|
||||
key2 = userProfile,
|
||||
) {
|
||||
value = room.userDisplayName(roomMemberId)
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
// Fallback to user profile
|
||||
userProfile?.displayName
|
||||
}
|
||||
)
|
||||
value = room.userDisplayName(roomMemberId).getOrNull() ?: roomMember?.displayName
|
||||
}
|
||||
|
||||
val userAvatar: String? by produceState(
|
||||
initialValue = roomMember?.avatarUrl ?: userProfile?.avatarUrl,
|
||||
val roomUserAvatar: String? by produceState(
|
||||
initialValue = roomMember?.avatarUrl,
|
||||
key1 = roomMember,
|
||||
key2 = userProfile,
|
||||
) {
|
||||
value = room.userAvatarUrl(roomMemberId)
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
// Fallback to user profile
|
||||
userProfile?.avatarUrl
|
||||
}
|
||||
)
|
||||
value = room.userAvatarUrl(roomMemberId).getOrNull() ?: roomMember?.avatarUrl
|
||||
}
|
||||
|
||||
return UserProfileState(
|
||||
userId = roomMemberId,
|
||||
userName = userName,
|
||||
avatarUrl = userAvatar,
|
||||
isBlocked = isBlocked.value,
|
||||
startDmActionState = startDmActionState.value,
|
||||
displayConfirmationDialog = confirmationDialog,
|
||||
isCurrentUser = isCurrentUser,
|
||||
dmRoomId = dmRoomId,
|
||||
canCall = canCall,
|
||||
eventSink = ::handleEvents
|
||||
val userProfileState = userProfilePresenter.present()
|
||||
|
||||
return userProfileState.copy(
|
||||
userName = roomUserName ?: userProfileState.userName,
|
||||
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
@@ -25,6 +24,8 @@ import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||
import io.element.android.features.roomdetails.impl.RoomTopicState
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
@@ -82,7 +83,12 @@ class RoomDetailsPresenterTest {
|
||||
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
||||
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
||||
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
||||
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, FakeStartDMAction())
|
||||
return RoomMemberDetailsPresenter(
|
||||
roomMemberId = roomMemberId,
|
||||
room = room,
|
||||
userProfileStatePresenterFactory = {
|
||||
Presenter { aUserProfileState() }
|
||||
})
|
||||
}
|
||||
}
|
||||
val featureFlagService = FakeFeatureFlagService(
|
||||
|
||||
@@ -9,27 +9,18 @@ package io.element.android.features.roomdetails.members.details
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||
import io.element.android.features.roomdetails.aMatrixRoom
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.features.userprofile.api.UserProfileStatePresenterFactory
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.MatrixRoomMembersState
|
||||
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_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -54,28 +45,26 @@ class RoomMemberDetailsPresenterTest {
|
||||
}
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = room,
|
||||
roomMemberId = roomMember.userId
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.userId).isEqualTo(roomMember.userId)
|
||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(roomMember.isIgnored))
|
||||
assertThat(initialState.dmRoomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(initialState.canCall).isFalse()
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userName).isEqualTo("Alice")
|
||||
assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url")
|
||||
skipItems(1)
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.userName).isEqualTo("A custom name")
|
||||
assertThat(loadedState.avatarUrl).isEqualTo("A custom avatar")
|
||||
val nextState = awaitItem()
|
||||
assertThat(nextState.userName).isEqualTo("A custom name")
|
||||
assertThat(nextState.avatarUrl).isEqualTo("A custom avatar")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - will recover when retrieving room member details fails`() = runTest {
|
||||
val roomMember = aRoomMember(displayName = "Alice")
|
||||
val roomMember = aRoomMember(
|
||||
displayName = "Alice",
|
||||
avatarUrl = "Alice Avatar url",
|
||||
)
|
||||
val room = aMatrixRoom(
|
||||
userDisplayNameResult = { Result.failure(Throwable()) },
|
||||
userAvatarUrlResult = { Result.failure(Throwable()) },
|
||||
@@ -86,16 +75,13 @@ class RoomMemberDetailsPresenterTest {
|
||||
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = room,
|
||||
roomMemberId = roomMember.userId
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userName).isEqualTo("Alice")
|
||||
assertThat(initialState.avatarUrl).isEqualTo("Alice Avatar url")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,238 +97,81 @@ class RoomMemberDetailsPresenterTest {
|
||||
}
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = room,
|
||||
roomMemberId = roomMember.userId
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userName).isEqualTo("Alice")
|
||||
assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - will fallback to user profile if user is not a member of the room`() = runTest {
|
||||
val bobProfile = aMatrixUser("@bob:server.org", "Bob", avatarUrl = "anAvatarUrl")
|
||||
val room = aMatrixRoom(
|
||||
userDisplayNameResult = { Result.failure(Exception("Not a member!")) },
|
||||
userAvatarUrlResult = { Result.failure(Exception("Not a member!")) },
|
||||
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
||||
)
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetProfileResult(bobProfile.userId, Result.success(bobProfile))
|
||||
}
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
client = client,
|
||||
room = room,
|
||||
roomMemberId = UserId("@bob:server.org")
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.userName).isEqualTo("Bob")
|
||||
assertThat(initialState.avatarUrl).isEqualTo("anAvatarUrl")
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userName).isEqualTo("Profile user name")
|
||||
assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||
fun `present - null cases`() = runTest {
|
||||
val roomMember = aRoomMember(
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
)
|
||||
val room = aMatrixRoom(
|
||||
userDisplayNameResult = { Result.success(null) },
|
||||
userAvatarUrlResult = { Result.success(null) },
|
||||
getUpdatedMemberResult = { Result.success(roomMember) },
|
||||
)
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = aMatrixRoom(
|
||||
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
||||
userDisplayNameResult = { Result.success("Alice") },
|
||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
||||
)
|
||||
room = room,
|
||||
userProfileStatePresenterFactory = {
|
||||
Presenter {
|
||||
aUserProfileState(
|
||||
userName = null,
|
||||
avatarUrl = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
|
||||
|
||||
val dialogState = awaitItem()
|
||||
assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Block)
|
||||
|
||||
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
|
||||
assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userName).isNull()
|
||||
assertThat(initialState.avatarUrl).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
|
||||
val client = FakeMatrixClient()
|
||||
val roomMember = aRoomMember()
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = aMatrixRoom(
|
||||
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
||||
userDisplayNameResult = { Result.success("Alice") },
|
||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
||||
),
|
||||
client = client,
|
||||
roomMemberId = roomMember.userId
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||
client.emitIgnoreUserList(listOf(roomMember.userId))
|
||||
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
|
||||
|
||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||
client.emitIgnoreUserList(listOf())
|
||||
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - BlockUser with error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
client = matrixClient,
|
||||
room = aMatrixRoom(
|
||||
getUpdatedMemberResult = { Result.success(aRoomMember(displayName = "Alice")) },
|
||||
userDisplayNameResult = { Result.success("Alice") },
|
||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||
skipItems(2)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
|
||||
// Clear error
|
||||
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
|
||||
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - UnblockUser with error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = aMatrixRoom(
|
||||
getUpdatedMemberResult = { Result.success(aRoomMember(displayName = "Alice")) },
|
||||
userDisplayNameResult = { Result.success("Alice") },
|
||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
||||
),
|
||||
client = matrixClient,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||
skipItems(2)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
|
||||
// Clear error
|
||||
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
|
||||
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = aMatrixRoom(
|
||||
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
||||
userDisplayNameResult = { Result.success("Alice") },
|
||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
|
||||
|
||||
val dialogState = awaitItem()
|
||||
assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Unblock)
|
||||
|
||||
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
|
||||
assertThat(awaitItem().displayConfirmationDialog).isNull()
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action complete scenario`() = runTest {
|
||||
val startDMAction = FakeStartDMAction()
|
||||
val presenter = createRoomMemberDetailsPresenter(
|
||||
room = aMatrixRoom(
|
||||
getUpdatedMemberResult = { Result.success(aRoomMember(displayName = "Alice")) },
|
||||
userDisplayNameResult = { Result.success("Alice") },
|
||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
||||
),
|
||||
startDMAction = startDMAction,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
||||
|
||||
// Failure
|
||||
startDMAction.givenExecuteResult(startDMFailureResult)
|
||||
initialState.eventSink(UserProfileEvents.StartDM)
|
||||
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
|
||||
state.eventSink(UserProfileEvents.ClearStartDMState)
|
||||
}
|
||||
|
||||
// Success
|
||||
startDMAction.givenExecuteResult(startDMSuccessResult)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized)
|
||||
state.eventSink(UserProfileEvents.StartDM)
|
||||
}
|
||||
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createRoomMemberDetailsPresenter(
|
||||
room: MatrixRoom,
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
roomMemberId: UserId = UserId("@alice:server.org"),
|
||||
startDMAction: StartDMAction = FakeStartDMAction()
|
||||
userProfileStatePresenterFactory: UserProfileStatePresenterFactory = UserProfileStatePresenterFactory {
|
||||
Presenter {
|
||||
aUserProfileState(
|
||||
userName = "Profile user name",
|
||||
avatarUrl = "Profile avatar url",
|
||||
)
|
||||
}
|
||||
},
|
||||
): RoomMemberDetailsPresenter {
|
||||
return RoomMemberDetailsPresenter(
|
||||
roomMemberId = roomMemberId,
|
||||
client = client,
|
||||
roomMemberId = UserId("@alice:server.org"),
|
||||
room = room,
|
||||
startDMAction = startDMAction
|
||||
userProfileStatePresenterFactory = userProfileStatePresenterFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userprofile.shared
|
||||
package io.element.android.features.userprofile.api
|
||||
|
||||
sealed interface UserProfileEvents {
|
||||
data object StartDM : UserProfileEvents
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userprofile.shared
|
||||
package io.element.android.features.userprofile.api
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userprofile.api
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
fun interface UserProfileStatePresenterFactory {
|
||||
fun create(userId: UserId): Presenter<UserProfileState>
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userprofile.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.api.UserProfileStatePresenterFactory
|
||||
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultUserProfileStatePresenterFactory @Inject constructor(
|
||||
private val factory: UserProfilePresenter.Factory,
|
||||
) : UserProfileStatePresenterFactory {
|
||||
override fun create(userId: UserId): Presenter<UserProfileState> = factory.create(userId)
|
||||
}
|
||||
@@ -19,10 +19,10 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog
|
||||
import io.element.android.features.userprofile.shared.UserProfilePresenterHelper
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
@@ -55,6 +55,7 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||
@Composable
|
||||
override fun present(): UserProfileState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val isCurrentUser = remember { client.isMe(userId) }
|
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
|
||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
@@ -112,7 +113,7 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||
isBlocked = isBlocked.value,
|
||||
startDmActionState = startDmActionState.value,
|
||||
displayConfirmationDialog = confirmationDialog,
|
||||
isCurrentUser = client.isMe(userId),
|
||||
isCurrentUser = isCurrentUser,
|
||||
dmRoomId = dmRoomId,
|
||||
canCall = canCall,
|
||||
eventSink = ::handleEvents
|
||||
|
||||
@@ -14,9 +14,9 @@ import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
package io.element.android.features.userprofile.shared
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
|
||||
@@ -9,9 +9,9 @@ package io.element.android.features.userprofile.shared.blockuser
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.R
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -14,9 +14,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.R
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
@@ -34,23 +34,24 @@ fun BlockUserSection(
|
||||
state: UserProfileState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isBlocked = state.isBlocked
|
||||
PreferenceCategory(
|
||||
modifier = modifier,
|
||||
showTopDivider = false,
|
||||
) {
|
||||
when (state.isBlocked) {
|
||||
is AsyncData.Failure -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
|
||||
is AsyncData.Loading -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
|
||||
is AsyncData.Success -> PreferenceBlockUser(isBlocked = state.isBlocked.data, isLoading = false, eventSink = state.eventSink)
|
||||
when (isBlocked) {
|
||||
is AsyncData.Failure -> PreferenceBlockUser(isBlocked = isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
|
||||
is AsyncData.Loading -> PreferenceBlockUser(isBlocked = isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
|
||||
is AsyncData.Success -> PreferenceBlockUser(isBlocked = isBlocked.data, isLoading = false, eventSink = state.eventSink)
|
||||
AsyncData.Uninitialized -> PreferenceBlockUser(isBlocked = null, isLoading = true, eventSink = state.eventSink)
|
||||
}
|
||||
}
|
||||
if (state.isBlocked is AsyncData.Failure) {
|
||||
if (isBlocked is AsyncData.Failure) {
|
||||
RetryDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onDismiss = { state.eventSink(UserProfileEvents.ClearBlockUserError) },
|
||||
onRetry = {
|
||||
val event = when (state.isBlocked.prevData) {
|
||||
val event = when (isBlocked.prevData) {
|
||||
true -> UserProfileEvents.UnblockUser(needsConfirmation = false)
|
||||
false -> UserProfileEvents.BlockUser(needsConfirmation = false)
|
||||
// null case Should not happen
|
||||
|
||||
@@ -13,9 +13,9 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.R
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.UserProfileView
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
@@ -10,9 +10,9 @@ package io.element.android.features.userprofile.shared.blockuser
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.R
|
||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
||||
import io.element.android.features.userprofile.shared.UserProfileState
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
|
||||
Reference in New Issue
Block a user