Merge pull request #3700 from element-hq/feature/bma/removeDuplicatedCode
Remove duplicated code regarding user (room member and user profile) screens
This commit is contained in:
commit
fa1a1975f2
20 changed files with 283 additions and 504 deletions
|
|
@ -9,7 +9,7 @@ package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
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.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
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.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
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.features.userprofile.shared.aUserProfileState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
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.core.RoomId
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,9 @@ package io.element.android.features.roomdetails.impl.di
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
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.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.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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
||||||
|
|
@ -22,13 +21,16 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
object RoomMemberModule {
|
object RoomMemberModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRoomMemberDetailsPresenterFactory(
|
fun provideRoomMemberDetailsPresenterFactory(
|
||||||
matrixClient: MatrixClient,
|
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
startDMAction: StartDMAction,
|
userProfilePresenterFactory: UserProfilePresenterFactory,
|
||||||
): RoomMemberDetailsPresenter.Factory {
|
): RoomMemberDetailsPresenter.Factory {
|
||||||
return object : RoomMemberDetailsPresenter.Factory {
|
return object : RoomMemberDetailsPresenter.Factory {
|
||||||
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
||||||
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction)
|
return RoomMemberDetailsPresenter(
|
||||||
|
roomMemberId = roomMemberId,
|
||||||
|
room = room,
|
||||||
|
userProfilePresenterFactory = userProfilePresenterFactory,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,153 +9,60 @@ package io.element.android.features.roomdetails.impl.members.details
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
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.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.createroom.api.StartDMAction
|
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
|
||||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
import io.element.android.features.userprofile.api.UserProfileState
|
||||||
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
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
|
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(
|
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomMemberId: UserId,
|
@Assisted private val roomMemberId: UserId,
|
||||||
private val client: MatrixClient,
|
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val startDMAction: StartDMAction,
|
userProfilePresenterFactory: UserProfilePresenterFactory,
|
||||||
) : Presenter<UserProfileState> {
|
) : Presenter<UserProfileState> {
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
|
fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val userProfilePresenterHelper = UserProfilePresenterHelper(
|
private val userProfilePresenter = userProfilePresenterFactory.create(roomMemberId)
|
||||||
userId = roomMemberId,
|
|
||||||
client = client,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserProfileState {
|
override fun present(): UserProfileState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
|
||||||
val roomMember by room.getRoomMemberAsState(roomMemberId)
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
// Update room member info when opening this screen
|
// 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`
|
// We don't need to assign the result as it will be automatically propagated by `room.getRoomMemberAsState`
|
||||||
room.getUpdatedMember(roomMemberId)
|
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) {
|
val roomUserName: String? by produceState(
|
||||||
when (event) {
|
initialValue = roomMember?.displayName,
|
||||||
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,
|
|
||||||
key1 = roomMember,
|
key1 = roomMember,
|
||||||
key2 = userProfile,
|
|
||||||
) {
|
) {
|
||||||
value = room.userDisplayName(roomMemberId)
|
value = room.userDisplayName(roomMemberId).getOrNull() ?: roomMember?.displayName
|
||||||
.fold(
|
|
||||||
onSuccess = { it },
|
|
||||||
onFailure = {
|
|
||||||
// Fallback to user profile
|
|
||||||
userProfile?.displayName
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val userAvatar: String? by produceState(
|
val roomUserAvatar: String? by produceState(
|
||||||
initialValue = roomMember?.avatarUrl ?: userProfile?.avatarUrl,
|
initialValue = roomMember?.avatarUrl,
|
||||||
key1 = roomMember,
|
key1 = roomMember,
|
||||||
key2 = userProfile,
|
|
||||||
) {
|
) {
|
||||||
value = room.userAvatarUrl(roomMemberId)
|
value = room.userAvatarUrl(roomMemberId).getOrNull() ?: roomMember?.avatarUrl
|
||||||
.fold(
|
|
||||||
onSuccess = { it },
|
|
||||||
onFailure = {
|
|
||||||
// Fallback to user profile
|
|
||||||
userProfile?.avatarUrl
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserProfileState(
|
val userProfileState = userProfilePresenter.present()
|
||||||
userId = roomMemberId,
|
|
||||||
userName = userName,
|
return userProfileState.copy(
|
||||||
avatarUrl = userAvatar,
|
userName = roomUserName ?: userProfileState.userName,
|
||||||
isBlocked = isBlocked.value,
|
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
|
||||||
startDmActionState = startDmActionState.value,
|
|
||||||
displayConfirmationDialog = confirmationDialog,
|
|
||||||
isCurrentUser = isCurrentUser,
|
|
||||||
dmRoomId = dmRoomId,
|
|
||||||
canCall = canCall,
|
|
||||||
eventSink = ::handleEvents
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
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.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
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.RoomTopicState
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
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.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
|
@ -82,7 +83,13 @@ class RoomDetailsPresenterTest {
|
||||||
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
||||||
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
|
||||||
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
|
||||||
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, FakeStartDMAction())
|
return RoomMemberDetailsPresenter(
|
||||||
|
roomMemberId = roomMemberId,
|
||||||
|
room = room,
|
||||||
|
userProfilePresenterFactory = {
|
||||||
|
Presenter { aUserProfileState() }
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val featureFlagService = FakeFeatureFlagService(
|
val featureFlagService = FakeFeatureFlagService(
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,18 @@ package io.element.android.features.roomdetails.members.details
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.ReceiveTurbine
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.api.StartDMAction
|
|
||||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
|
||||||
import io.element.android.features.roomdetails.aMatrixRoom
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.features.userprofile.shared.UserProfileEvents
|
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
|
||||||
import io.element.android.features.userprofile.shared.UserProfileState
|
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
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.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 io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -54,28 +45,26 @@ class RoomMemberDetailsPresenterTest {
|
||||||
}
|
}
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
roomMemberId = roomMember.userId
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.userId).isEqualTo(roomMember.userId)
|
assertThat(initialState.userName).isEqualTo("Alice")
|
||||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url")
|
||||||
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()
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val loadedState = awaitItem()
|
val nextState = awaitItem()
|
||||||
assertThat(loadedState.userName).isEqualTo("A custom name")
|
assertThat(nextState.userName).isEqualTo("A custom name")
|
||||||
assertThat(loadedState.avatarUrl).isEqualTo("A custom avatar")
|
assertThat(nextState.avatarUrl).isEqualTo("A custom avatar")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - will recover when retrieving room member details fails`() = runTest {
|
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(
|
val room = aMatrixRoom(
|
||||||
userDisplayNameResult = { Result.failure(Throwable()) },
|
userDisplayNameResult = { Result.failure(Throwable()) },
|
||||||
userAvatarUrlResult = { Result.failure(Throwable()) },
|
userAvatarUrlResult = { Result.failure(Throwable()) },
|
||||||
|
|
@ -86,16 +75,13 @@ class RoomMemberDetailsPresenterTest {
|
||||||
|
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
roomMemberId = roomMember.userId
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
assertThat(initialState.userName).isEqualTo("Alice")
|
||||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
assertThat(initialState.avatarUrl).isEqualTo("Alice Avatar url")
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,238 +97,81 @@ class RoomMemberDetailsPresenterTest {
|
||||||
}
|
}
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
roomMemberId = roomMember.userId
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
assertThat(initialState.userName).isEqualTo("Alice")
|
||||||
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url")
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - will fallback to user profile if user is not a member of the room`() = runTest {
|
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(
|
val room = aMatrixRoom(
|
||||||
userDisplayNameResult = { Result.failure(Exception("Not a member!")) },
|
userDisplayNameResult = { Result.failure(Exception("Not a member!")) },
|
||||||
userAvatarUrlResult = { Result.failure(Exception("Not a member!")) },
|
userAvatarUrlResult = { Result.failure(Exception("Not a member!")) },
|
||||||
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
||||||
)
|
)
|
||||||
val client = FakeMatrixClient().apply {
|
|
||||||
givenGetProfileResult(bobProfile.userId, Result.success(bobProfile))
|
|
||||||
}
|
|
||||||
val presenter = createRoomMemberDetailsPresenter(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
client = client,
|
|
||||||
room = room,
|
room = room,
|
||||||
roomMemberId = UserId("@bob:server.org")
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(2)
|
val initialState = awaitItem()
|
||||||
val initialState = awaitFirstItem()
|
assertThat(initialState.userName).isEqualTo("Profile user name")
|
||||||
assertThat(initialState.userName).isEqualTo("Bob")
|
assertThat(initialState.avatarUrl).isEqualTo("Profile avatar url")
|
||||||
assertThat(initialState.avatarUrl).isEqualTo("anAvatarUrl")
|
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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(
|
val presenter = createRoomMemberDetailsPresenter(
|
||||||
room = aMatrixRoom(
|
room = room,
|
||||||
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
|
userProfilePresenterFactory = {
|
||||||
userDisplayNameResult = { Result.success("Alice") },
|
Presenter {
|
||||||
userAvatarUrlResult = { Result.success("anAvatarUrl") },
|
aUserProfileState(
|
||||||
)
|
userName = null,
|
||||||
|
avatarUrl = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
|
assertThat(initialState.userName).isNull()
|
||||||
|
assertThat(initialState.avatarUrl).isNull()
|
||||||
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") },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
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(
|
private fun createRoomMemberDetailsPresenter(
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
userProfilePresenterFactory: UserProfilePresenterFactory = UserProfilePresenterFactory {
|
||||||
roomMemberId: UserId = UserId("@alice:server.org"),
|
Presenter {
|
||||||
startDMAction: StartDMAction = FakeStartDMAction()
|
aUserProfileState(
|
||||||
|
userName = "Profile user name",
|
||||||
|
avatarUrl = "Profile avatar url",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
): RoomMemberDetailsPresenter {
|
): RoomMemberDetailsPresenter {
|
||||||
return RoomMemberDetailsPresenter(
|
return RoomMemberDetailsPresenter(
|
||||||
roomMemberId = roomMemberId,
|
roomMemberId = UserId("@alice:server.org"),
|
||||||
client = client,
|
|
||||||
room = room,
|
room = room,
|
||||||
startDMAction = startDMAction
|
userProfilePresenterFactory = userProfilePresenterFactory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE in the repository root for full details.
|
* 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 {
|
sealed interface UserProfileEvents {
|
||||||
data object StartDM : UserProfileEvents
|
data object StartDM : UserProfileEvents
|
||||||
|
|
@ -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<UserProfileState>
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE in the repository root for full details.
|
* 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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -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<UserProfileState> = factory.create(userId)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,18 +10,20 @@ package io.element.android.features.userprofile.impl.root
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.createroom.api.StartDMAction
|
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.shared.UserProfilePresenterHelper
|
import io.element.android.features.userprofile.api.UserProfileState
|
||||||
import io.element.android.features.userprofile.shared.UserProfileState
|
import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog
|
||||||
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -41,24 +44,39 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val startDMAction: StartDMAction,
|
private val startDMAction: StartDMAction,
|
||||||
) : Presenter<UserProfileState> {
|
) : Presenter<UserProfileState> {
|
||||||
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(userId: UserId): UserProfilePresenter
|
fun create(userId: UserId): UserProfilePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val userProfilePresenterHelper = UserProfilePresenterHelper(
|
@Composable
|
||||||
userId = userId,
|
private fun getDmRoomId(): State<RoomId?> {
|
||||||
client = client,
|
return produceState<RoomId?>(initialValue = null) {
|
||||||
)
|
value = client.findDM(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getCanCall(roomId: RoomId?): State<Boolean> {
|
||||||
|
return produceState(initialValue = false, roomId) {
|
||||||
|
value = if (client.isMe(userId)) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
roomId?.let { client.getRoom(it)?.canUserJoinCall(client.sessionId)?.getOrNull() == true }.orFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserProfileState {
|
override fun present(): UserProfileState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val isCurrentUser = remember { client.isMe(userId) }
|
||||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
|
||||||
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
|
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
|
||||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||||
val dmRoomId by userProfilePresenterHelper.getDmRoomId()
|
val dmRoomId by getDmRoomId()
|
||||||
val canCall by userProfilePresenterHelper.getCanCall(dmRoomId)
|
val canCall by getCanCall(dmRoomId)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
client.ignoredUsersFlow
|
client.ignoredUsersFlow
|
||||||
.map { ignoredUsers -> userId in ignoredUsers }
|
.map { ignoredUsers -> userId in ignoredUsers }
|
||||||
|
|
@ -77,7 +95,7 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||||
confirmationDialog = ConfirmationDialog.Block
|
confirmationDialog = ConfirmationDialog.Block
|
||||||
} else {
|
} else {
|
||||||
confirmationDialog = null
|
confirmationDialog = null
|
||||||
userProfilePresenterHelper.blockUser(coroutineScope, isBlocked)
|
coroutineScope.blockUser(isBlocked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UserProfileEvents.UnblockUser -> {
|
is UserProfileEvents.UnblockUser -> {
|
||||||
|
|
@ -85,7 +103,7 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||||
confirmationDialog = ConfirmationDialog.Unblock
|
confirmationDialog = ConfirmationDialog.Unblock
|
||||||
} else {
|
} else {
|
||||||
confirmationDialog = null
|
confirmationDialog = null
|
||||||
userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked)
|
coroutineScope.unblockUser(isBlocked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
|
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
|
||||||
|
|
@ -110,10 +128,32 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||||
isBlocked = isBlocked.value,
|
isBlocked = isBlocked.value,
|
||||||
startDmActionState = startDmActionState.value,
|
startDmActionState = startDmActionState.value,
|
||||||
displayConfirmationDialog = confirmationDialog,
|
displayConfirmationDialog = confirmationDialog,
|
||||||
isCurrentUser = client.isMe(userId),
|
isCurrentUser = isCurrentUser,
|
||||||
dmRoomId = dmRoomId,
|
dmRoomId = dmRoomId,
|
||||||
canCall = canCall,
|
canCall = canCall,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.blockUser(
|
||||||
|
isBlockedState: MutableState<AsyncData<Boolean>>,
|
||||||
|
) = 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<AsyncData<Boolean>>,
|
||||||
|
) = launch {
|
||||||
|
isBlockedState.value = AsyncData.Loading(true)
|
||||||
|
client.unignoreUser(userId)
|
||||||
|
.onFailure {
|
||||||
|
isBlockedState.value = AsyncData.Failure(it, true)
|
||||||
|
}
|
||||||
|
// Note: on success, ignoredUsersFlow will emit new item.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,29 @@
|
||||||
|
|
||||||
package io.element.android.features.userprofile.impl
|
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.ReceiveTurbine
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.api.StartDMAction
|
import io.element.android.features.createroom.api.StartDMAction
|
||||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
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.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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
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
|
||||||
|
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.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -47,9 +49,7 @@ class UserProfilePresenterTest {
|
||||||
val presenter = createUserProfilePresenter(
|
val presenter = createUserProfilePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.userId).isEqualTo(matrixUser.userId)
|
assertThat(initialState.userId).isEqualTo(matrixUser.userId)
|
||||||
assertThat(initialState.userName).isEqualTo(matrixUser.displayName)
|
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<Boolean> = 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
|
@Test
|
||||||
fun `present - returns empty data in case of failure`() = runTest {
|
fun `present - returns empty data in case of failure`() = runTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
|
|
@ -68,9 +132,7 @@ class UserProfilePresenterTest {
|
||||||
val presenter = createUserProfilePresenter(
|
val presenter = createUserProfilePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.userId).isEqualTo(A_USER_ID)
|
assertThat(initialState.userId).isEqualTo(A_USER_ID)
|
||||||
assertThat(initialState.userName).isNull()
|
assertThat(initialState.userName).isNull()
|
||||||
|
|
@ -82,9 +144,7 @@ class UserProfilePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||||
val presenter = createUserProfilePresenter()
|
val presenter = createUserProfilePresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
|
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
|
||||||
|
|
||||||
|
|
@ -105,9 +165,7 @@ class UserProfilePresenterTest {
|
||||||
client = client,
|
client = client,
|
||||||
userId = A_USER_ID
|
userId = A_USER_ID
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
|
|
@ -126,9 +184,7 @@ class UserProfilePresenterTest {
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
|
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
|
||||||
val presenter = createUserProfilePresenter(client = matrixClient)
|
val presenter = createUserProfilePresenter(client = matrixClient)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
|
|
@ -145,9 +201,7 @@ class UserProfilePresenterTest {
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
|
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
|
||||||
val presenter = createUserProfilePresenter(client = matrixClient)
|
val presenter = createUserProfilePresenter(client = matrixClient)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
||||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
|
||||||
|
|
@ -162,9 +216,7 @@ class UserProfilePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
|
||||||
val presenter = createUserProfilePresenter()
|
val presenter = createUserProfilePresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
|
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
|
||||||
|
|
||||||
|
|
@ -182,9 +234,7 @@ class UserProfilePresenterTest {
|
||||||
fun `present - start DM action complete scenario`() = runTest {
|
fun `present - start DM action complete scenario`() = runTest {
|
||||||
val startDMAction = FakeStartDMAction()
|
val startDMAction = FakeStartDMAction()
|
||||||
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
|
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||||
|
|
|
||||||
|
|
@ -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<RoomId?> {
|
|
||||||
return produceState<RoomId?>(initialValue = null) {
|
|
||||||
value = client.findDM(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun getCanCall(roomId: RoomId?): State<Boolean> {
|
|
||||||
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<AsyncData<Boolean>>,
|
|
||||||
) = 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<AsyncData<Boolean>>,
|
|
||||||
) = scope.launch {
|
|
||||||
isBlockedState.value = AsyncData.Loading(true)
|
|
||||||
client.unignoreUser(userId)
|
|
||||||
.onFailure {
|
|
||||||
isBlockedState.value = AsyncData.Failure(it, true)
|
|
||||||
}
|
|
||||||
// Note: on success, ignoredUserList will be updated.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
package io.element.android.features.userprofile.shared
|
package io.element.android.features.userprofile.shared
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
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.BlockUserDialogs
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
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.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
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.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.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
|
|
@ -34,23 +34,24 @@ fun BlockUserSection(
|
||||||
state: UserProfileState,
|
state: UserProfileState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val isBlocked = state.isBlocked
|
||||||
PreferenceCategory(
|
PreferenceCategory(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
showTopDivider = false,
|
showTopDivider = false,
|
||||||
) {
|
) {
|
||||||
when (state.isBlocked) {
|
when (isBlocked) {
|
||||||
is AsyncData.Failure -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
|
is AsyncData.Failure -> PreferenceBlockUser(isBlocked = isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
|
||||||
is AsyncData.Loading -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
|
is AsyncData.Loading -> PreferenceBlockUser(isBlocked = isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
|
||||||
is AsyncData.Success -> PreferenceBlockUser(isBlocked = state.isBlocked.data, isLoading = false, 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)
|
AsyncData.Uninitialized -> PreferenceBlockUser(isBlocked = null, isLoading = true, eventSink = state.eventSink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.isBlocked is AsyncData.Failure) {
|
if (isBlocked is AsyncData.Failure) {
|
||||||
RetryDialog(
|
RetryDialog(
|
||||||
content = stringResource(CommonStrings.error_unknown),
|
content = stringResource(CommonStrings.error_unknown),
|
||||||
onDismiss = { state.eventSink(UserProfileEvents.ClearBlockUserError) },
|
onDismiss = { state.eventSink(UserProfileEvents.ClearBlockUserError) },
|
||||||
onRetry = {
|
onRetry = {
|
||||||
val event = when (state.isBlocked.prevData) {
|
val event = when (isBlocked.prevData) {
|
||||||
true -> UserProfileEvents.UnblockUser(needsConfirmation = false)
|
true -> UserProfileEvents.UnblockUser(needsConfirmation = false)
|
||||||
false -> UserProfileEvents.BlockUser(needsConfirmation = false)
|
false -> UserProfileEvents.BlockUser(needsConfirmation = false)
|
||||||
// null case Should not happen
|
// 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.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.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.UserProfileView
|
||||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.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.features.userprofile.shared.aUserProfileState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue