diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index f33b647a13..1abaa59d93 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -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 diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 3746407fbf..d9b0c22c65 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -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 diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt index cf59f0db1c..c1e4369aaa 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -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.UserProfilePresenterFactory 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,16 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom object RoomMemberModule { @Provides fun provideRoomMemberDetailsPresenterFactory( - matrixClient: MatrixClient, room: MatrixRoom, - startDMAction: StartDMAction, + userProfilePresenterFactory: UserProfilePresenterFactory, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction) + return RoomMemberDetailsPresenter( + roomMemberId = roomMemberId, + room = room, + userProfilePresenterFactory = userProfilePresenterFactory, + ) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 768c717e73..0e6d9052f0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -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.UserProfilePresenterFactory +import io.element.android.features.userprofile.api.UserProfileState 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 UserProfilePresenter, 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, + userProfilePresenterFactory: UserProfilePresenterFactory, ) : Presenter { interface Factory { fun create(roomMemberId: UserId): RoomMemberDetailsPresenter } - private val userProfilePresenterHelper = UserProfilePresenterHelper( - userId = roomMemberId, - client = client, - ) + private val userProfilePresenter = userProfilePresenterFactory.create(roomMemberId) @Composable override fun present(): UserProfileState { - val coroutineScope = rememberCoroutineScope() - var confirmationDialog by remember { mutableStateOf(null) } val roomMember by room.getRoomMemberAsState(roomMemberId) - var userProfile by remember { mutableStateOf(null) } - val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - val isBlocked: MutableState> = 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, ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt index 73eee2092e..9a893b671e 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt @@ -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,13 @@ 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, + userProfilePresenterFactory = { + Presenter { aUserProfileState() } + }, + ) } } val featureFlagService = FakeFeatureFlagService( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt index c987e308ab..1ea63f6cce 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt @@ -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.UserProfilePresenterFactory +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 { - val presenter = createRoomMemberDetailsPresenter( - room = aMatrixRoom( - getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) }, - userDisplayNameResult = { Result.success("Alice") }, - userAvatarUrlResult = { Result.success("anAvatarUrl") }, - ) + fun `present - null cases`() = runTest { + val roomMember = aRoomMember( + displayName = 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() - } - } - - @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") }, - ), + val room = aMatrixRoom( + userDisplayNameResult = { Result.success(null) }, + userAvatarUrlResult = { Result.success(null) }, + getUpdatedMemberResult = { Result.success(roomMember) }, ) - 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, + room = room, + userProfilePresenterFactory = { + Presenter { + aUserProfileState( + userName = null, + avatarUrl = null, + ) + } + }, ) 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) - } + val initialState = awaitItem() + assertThat(initialState.userName).isNull() + assertThat(initialState.avatarUrl).isNull() } } - private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(1) - return awaitItem() - } - private fun createRoomMemberDetailsPresenter( room: MatrixRoom, - client: MatrixClient = FakeMatrixClient(), - roomMemberId: UserId = UserId("@alice:server.org"), - startDMAction: StartDMAction = FakeStartDMAction() + userProfilePresenterFactory: UserProfilePresenterFactory = UserProfilePresenterFactory { + 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 + userProfilePresenterFactory = userProfilePresenterFactory ) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileEvents.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt similarity index 91% rename from features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileEvents.kt rename to features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt index da096f6288..a277eb5733 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileEvents.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt @@ -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 diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt new file mode 100644 index 0000000000..6142d5f225 --- /dev/null +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt @@ -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 UserProfilePresenterFactory { + fun create(userId: UserId): Presenter +} diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileState.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt similarity index 93% rename from features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileState.kt rename to features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt index ceb3cd7952..4bc3dc06d7 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileState.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt @@ -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 diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt new file mode 100644 index 0000000000..c0f32b5df7 --- /dev/null +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfilePresenterFactory.kt @@ -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.UserProfilePresenterFactory +import io.element.android.features.userprofile.api.UserProfileState +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 DefaultUserProfilePresenterFactory @Inject constructor( + private val factory: UserProfilePresenter.Factory, +) : UserProfilePresenterFactory { + override fun create(userId: UserId): Presenter = factory.create(userId) +} diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/di/UserProfileModule.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/di/UserProfileModule.kt deleted file mode 100644 index ae30b10175..0000000000 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/di/UserProfileModule.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.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.userprofile.impl.root.UserProfilePresenter -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.UserId - -@Module -@ContributesTo(SessionScope::class) -object UserProfileModule { - @Provides - fun provideUserProfilePresenterFactory( - matrixClient: MatrixClient, - startDMAction: StartDMAction, - ): UserProfilePresenter.Factory { - return object : UserProfilePresenter.Factory { - override fun create(userId: UserId): UserProfilePresenter { - return UserProfilePresenter(userId, matrixClient, startDMAction) - } - } - } -} diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 136ffcb206..e73eef6aa0 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -10,18 +10,20 @@ package io.element.android.features.userprofile.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State 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.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.shared.UserProfilePresenterHelper -import io.element.android.features.userprofile.shared.UserProfileState -import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog +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.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -30,6 +32,7 @@ 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.user.MatrixUser +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -41,24 +44,39 @@ class UserProfilePresenter @AssistedInject constructor( private val client: MatrixClient, private val startDMAction: StartDMAction, ) : Presenter { + @AssistedFactory interface Factory { fun create(userId: UserId): UserProfilePresenter } - private val userProfilePresenterHelper = UserProfilePresenterHelper( - userId = userId, - client = client, - ) + @Composable + private fun getDmRoomId(): State { + return produceState(initialValue = null) { + value = client.findDM(userId) + } + } + + @Composable + private fun getCanCall(roomId: RoomId?): State { + return produceState(initialValue = false, roomId) { + value = if (client.isMe(userId)) { + false + } else { + roomId?.let { client.getRoom(it)?.canUserJoinCall(client.sessionId)?.getOrNull() == true }.orFalse() + } + } + } @Composable override fun present(): UserProfileState { val coroutineScope = rememberCoroutineScope() + val isCurrentUser = remember { client.isMe(userId) } var confirmationDialog by remember { mutableStateOf(null) } var userProfile by remember { mutableStateOf(null) } val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val isBlocked: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } - val dmRoomId by userProfilePresenterHelper.getDmRoomId() - val canCall by userProfilePresenterHelper.getCanCall(dmRoomId) + val dmRoomId by getDmRoomId() + val canCall by getCanCall(dmRoomId) LaunchedEffect(Unit) { client.ignoredUsersFlow .map { ignoredUsers -> userId in ignoredUsers } @@ -77,7 +95,7 @@ class UserProfilePresenter @AssistedInject constructor( confirmationDialog = ConfirmationDialog.Block } else { confirmationDialog = null - userProfilePresenterHelper.blockUser(coroutineScope, isBlocked) + coroutineScope.blockUser(isBlocked) } } is UserProfileEvents.UnblockUser -> { @@ -85,7 +103,7 @@ class UserProfilePresenter @AssistedInject constructor( confirmationDialog = ConfirmationDialog.Unblock } else { confirmationDialog = null - userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked) + coroutineScope.unblockUser(isBlocked) } } UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null @@ -110,10 +128,32 @@ class UserProfilePresenter @AssistedInject constructor( isBlocked = isBlocked.value, startDmActionState = startDmActionState.value, displayConfirmationDialog = confirmationDialog, - isCurrentUser = client.isMe(userId), + isCurrentUser = isCurrentUser, dmRoomId = dmRoomId, canCall = canCall, eventSink = ::handleEvents ) } + + private fun CoroutineScope.blockUser( + isBlockedState: MutableState>, + ) = launch { + isBlockedState.value = AsyncData.Loading(false) + client.ignoreUser(userId) + .onFailure { + isBlockedState.value = AsyncData.Failure(it, false) + } + // Note: on success, ignoredUsersFlow will emit new item. + } + + private fun CoroutineScope.unblockUser( + isBlockedState: MutableState>, + ) = launch { + isBlockedState.value = AsyncData.Loading(true) + client.unignoreUser(userId) + .onFailure { + isBlockedState.value = AsyncData.Failure(it, true) + } + // Note: on success, ignoredUsersFlow will emit new item. + } } diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index 043ea267d7..1da38187e1 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -7,27 +7,29 @@ package io.element.android.features.userprofile.impl -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.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 +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId 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.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -47,9 +49,7 @@ class UserProfilePresenterTest { val presenter = createUserProfilePresenter( client = client, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.userId).isEqualTo(matrixUser.userId) assertThat(initialState.userName).isEqualTo(matrixUser.displayName) @@ -60,6 +60,70 @@ class UserProfilePresenterTest { } } + @Test + fun `present - canCall is true when all the conditions are met`() { + testCanCall( + expectedResult = true, + ) + } + + @Test + fun `present - canCall is false when canUserJoinCall returns false`() { + testCanCall( + canUserJoinCallResult = Result.success(false), + expectedResult = false, + ) + } + + @Test + fun `present - canCall is false when canUserJoinCall fails`() { + testCanCall( + canUserJoinCallResult = Result.failure(AN_EXCEPTION), + expectedResult = false, + ) + } + + @Test + fun `present - canCall is false when there is no DM`() { + testCanCall( + dmRoom = null, + expectedResult = false, + ) + } + + @Test + fun `present - canCall is false when room is not found`() { + testCanCall( + canFindRoom = false, + expectedResult = false, + ) + } + + private fun testCanCall( + canUserJoinCallResult: Result = Result.success(true), + dmRoom: RoomId? = A_ROOM_ID, + canFindRoom: Boolean = true, + expectedResult: Boolean, + ) = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { canUserJoinCallResult }, + ) + val client = FakeMatrixClient().apply { + if (canFindRoom) { + givenGetRoomResult(A_ROOM_ID, room) + } + givenFindDmResult(dmRoom) + } + val presenter = createUserProfilePresenter( + userId = A_USER_ID_2, + client = client, + ) + presenter.test { + val initialState = awaitLastSequentialItem() + assertThat(initialState.canCall).isEqualTo(expectedResult) + } + } + @Test fun `present - returns empty data in case of failure`() = runTest { val client = FakeMatrixClient().apply { @@ -68,9 +132,7 @@ class UserProfilePresenterTest { val presenter = createUserProfilePresenter( client = client, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.userId).isEqualTo(A_USER_ID) assertThat(initialState.userName).isNull() @@ -82,9 +144,7 @@ class UserProfilePresenterTest { @Test fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { val presenter = createUserProfilePresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true)) @@ -105,9 +165,7 @@ class UserProfilePresenterTest { client = client, userId = A_USER_ID ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() @@ -126,9 +184,7 @@ class UserProfilePresenterTest { val matrixClient = FakeMatrixClient() matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE)) val presenter = createUserProfilePresenter(client = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() @@ -145,9 +201,7 @@ class UserProfilePresenterTest { val matrixClient = FakeMatrixClient() matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE)) val presenter = createUserProfilePresenter(client = matrixClient) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() @@ -162,9 +216,7 @@ class UserProfilePresenterTest { @Test fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { val presenter = createUserProfilePresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true)) @@ -182,9 +234,7 @@ class UserProfilePresenterTest { fun `present - start DM action complete scenario`() = runTest { val startDMAction = FakeStartDMAction() val presenter = createUserProfilePresenter(startDMAction = startDMAction) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfilePresenterHelper.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfilePresenterHelper.kt deleted file mode 100644 index 3b206900b2..0000000000 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfilePresenterHelper.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.shared - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.produceState -import io.element.android.libraries.architecture.AsyncData -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 kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class UserProfilePresenterHelper( - private val userId: UserId, - private val client: MatrixClient, -) { - @Composable - fun getDmRoomId(): State { - return produceState(initialValue = null) { - value = client.findDM(userId) - } - } - - @Composable - fun getCanCall(roomId: RoomId?): State { - return produceState(initialValue = false, roomId) { - value = if (client.isMe(userId)) { - false - } else { - roomId?.let { client.getRoom(it)?.canUserJoinCall(client.sessionId)?.getOrNull() == true }.orFalse() - } - } - } - - fun blockUser( - scope: CoroutineScope, - isBlockedState: MutableState>, - ) = scope.launch { - isBlockedState.value = AsyncData.Loading(false) - client.ignoreUser(userId) - .onFailure { - isBlockedState.value = AsyncData.Failure(it, false) - } - // Note: on success, ignoredUserList will be updated. - } - - fun unblockUser( - scope: CoroutineScope, - isBlockedState: MutableState>, - ) = scope.launch { - isBlockedState.value = AsyncData.Loading(true) - client.unignoreUser(userId) - .onFailure { - isBlockedState.value = AsyncData.Failure(it, true) - } - // Note: on success, ignoredUserList will be updated. - } -} diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt index 9126ae49ad..9fa1eca2dc 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt @@ -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 diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index 250ec0c86c..12aafb2731 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -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 diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt index b2e7ec259c..d4e8ad952d 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt @@ -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 diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt index 92c979b8bf..0ec3d5ee91 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt @@ -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 diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt index 2971904d89..38ebfc7960 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt @@ -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 diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt index 584713bc27..934258974e 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt @@ -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