UX cleanup: DM details screen (#2820)
* UX cleanup: user profile. - Move send DM to a CTA button. - Add 'Call' CTA button too when there is a DM with that user and a call is possible. - Add missing tests. * Update screenshots * Add tests for clicking on the avatar --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
committed by
GitHub
parent
17f5b0fe77
commit
2e93d842c7
1
changelog.d/2818.misc
Normal file
1
changelog.d/2818.misc
Normal file
@@ -0,0 +1 @@
|
||||
UX cleanup: user profile. Move send DM to a call to action button, add 'Call' CTA too.
|
||||
@@ -188,6 +188,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||
override fun onStartDM(roomId: RoomId) {
|
||||
plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
|
||||
override fun onStartCall(roomId: RoomId) {
|
||||
ElementCallActivity.start(context, CallType.RoomCall(roomId = roomId, sessionId = room.sessionId))
|
||||
}
|
||||
}
|
||||
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
|
||||
createNode<RoomMemberDetailsNode>(buildContext, plugins)
|
||||
|
||||
@@ -76,6 +76,10 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
||||
callback.onStartDM(roomId)
|
||||
}
|
||||
|
||||
fun onStartCall(roomId: RoomId) {
|
||||
callback.onStartCall(roomId)
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
|
||||
LaunchedEffect(state.startDmActionState) {
|
||||
@@ -89,7 +93,8 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
||||
modifier = modifier,
|
||||
goBack = this::navigateUp,
|
||||
onShareUser = ::onShareUser,
|
||||
onDMStarted = ::onStartDM,
|
||||
onDmStarted = ::onStartDM,
|
||||
onStartCall = ::onStartCall,
|
||||
openAvatarPreview = callback::openAvatarPreview,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
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 }
|
||||
@@ -158,7 +161,9 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||
isBlocked = isBlocked.value,
|
||||
startDmActionState = startDmActionState.value,
|
||||
displayConfirmationDialog = confirmationDialog,
|
||||
isCurrentUser = client.isMe(roomMemberId),
|
||||
isCurrentUser = isCurrentUser,
|
||||
dmRoomId = dmRoomId,
|
||||
canCall = canCall,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ class RoomMemberDetailsPresenterTests {
|
||||
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()
|
||||
skipItems(1)
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.userName).isEqualTo("A custom name")
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies {
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.mediaviewer.api)
|
||||
implementation(projects.features.call)
|
||||
api(projects.features.userprofile.api)
|
||||
api(projects.features.userprofile.shared)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.userprofile.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -28,6 +29,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.call.CallType
|
||||
import io.element.android.features.call.ui.ElementCallActivity
|
||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||
import io.element.android.features.userprofile.impl.root.UserProfileNode
|
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||
@@ -37,9 +40,11 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -48,6 +53,8 @@ import kotlinx.parcelize.Parcelize
|
||||
class UserProfileFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sessionIdHolder: CurrentSessionIdHolder,
|
||||
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
@@ -75,6 +82,10 @@ class UserProfileFlowNode @AssistedInject constructor(
|
||||
override fun onStartDM(roomId: RoomId) {
|
||||
plugins<UserProfileEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
|
||||
override fun onStartCall(roomId: RoomId) {
|
||||
ElementCallActivity.start(context, CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = roomId))
|
||||
}
|
||||
}
|
||||
val params = UserProfileNode.UserProfileInputs(userId = inputs<UserProfileEntryPoint.Params>().userId)
|
||||
createNode<UserProfileNode>(buildContext, listOf(callback, params))
|
||||
|
||||
@@ -89,7 +89,8 @@ class UserProfileNode @AssistedInject constructor(
|
||||
modifier = modifier,
|
||||
goBack = this::navigateUp,
|
||||
onShareUser = ::onShareUser,
|
||||
onDMStarted = ::onStartDM,
|
||||
onDmStarted = ::onStartDM,
|
||||
onStartCall = callback::onStartCall,
|
||||
openAvatarPreview = callback::openAvatarPreview,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||
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 dmRoomId by userProfilePresenterHelper.getDmRoomId()
|
||||
val canCall by userProfilePresenterHelper.getCanCall(dmRoomId)
|
||||
LaunchedEffect(Unit) {
|
||||
client.ignoredUsersFlow
|
||||
.map { ignoredUsers -> userId in ignoredUsers }
|
||||
@@ -118,6 +120,8 @@ class UserProfilePresenter @AssistedInject constructor(
|
||||
startDmActionState = startDmActionState.value,
|
||||
displayConfirmationDialog = confirmationDialog,
|
||||
isCurrentUser = client.isMe(userId),
|
||||
dmRoomId = dmRoomId,
|
||||
canCall = canCall,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ class UserProfilePresenterTests {
|
||||
assertThat(initialState.userName).isEqualTo(matrixUser.displayName)
|
||||
assertThat(initialState.avatarUrl).isEqualTo(matrixUser.avatarUrl)
|
||||
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false))
|
||||
assertThat(initialState.dmRoomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(initialState.canCall).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,12 @@ fun UserProfileHeaderSection(
|
||||
openAvatarPreview: (url: String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(modifier = Modifier.size(70.dp)) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
||||
@@ -65,6 +70,7 @@ fun UserProfileHeaderSection(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = userName,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
@@ -29,11 +29,32 @@ import io.element.android.libraries.designsystem.components.button.MainActionBut
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun UserProfileMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) {
|
||||
fun UserProfileMainActionsSection(
|
||||
isCurrentUser: Boolean,
|
||||
canCall: Boolean,
|
||||
onShareUser: () -> Unit,
|
||||
onStartDM: () -> Unit,
|
||||
onCall: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
if (!isCurrentUser) {
|
||||
MainActionButton(
|
||||
title = stringResource(CommonStrings.action_message),
|
||||
imageVector = CompoundIcons.Chat(),
|
||||
onClick = onStartDM,
|
||||
)
|
||||
}
|
||||
if (canCall) {
|
||||
MainActionButton(
|
||||
title = stringResource(CommonStrings.action_call),
|
||||
imageVector = CompoundIcons.VideoCall(),
|
||||
onClick = onCall,
|
||||
)
|
||||
}
|
||||
MainActionButton(
|
||||
title = stringResource(CommonStrings.action_share),
|
||||
imageVector = CompoundIcons.ShareAndroid(),
|
||||
|
||||
@@ -32,6 +32,7 @@ class UserProfileNodeHelper(
|
||||
interface Callback : NodeInputs {
|
||||
fun openAvatarPreview(username: String, avatarUrl: String)
|
||||
fun onStartDM(roomId: RoomId)
|
||||
fun onStartCall(roomId: RoomId)
|
||||
}
|
||||
|
||||
fun onShareUser(
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
|
||||
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
|
||||
@@ -27,6 +32,24 @@ 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>>,
|
||||
|
||||
@@ -29,6 +29,8 @@ data class UserProfileState(
|
||||
val startDmActionState: AsyncAction<RoomId>,
|
||||
val displayConfirmationDialog: ConfirmationDialog?,
|
||||
val isCurrentUser: Boolean,
|
||||
val dmRoomId: RoomId?,
|
||||
val canCall: Boolean,
|
||||
val eventSink: (UserProfileEvents) -> Unit
|
||||
) {
|
||||
enum class ConfirmationDialog {
|
||||
|
||||
@@ -32,6 +32,8 @@ open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState>
|
||||
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock),
|
||||
aUserProfileState(isBlocked = AsyncData.Loading(true)),
|
||||
aUserProfileState(startDmActionState = AsyncAction.Loading),
|
||||
aUserProfileState(canCall = true),
|
||||
aUserProfileState(dmRoomId = null),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
@@ -44,6 +46,8 @@ fun aUserProfileState(
|
||||
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
displayConfirmationDialog: UserProfileState.ConfirmationDialog? = null,
|
||||
isCurrentUser: Boolean = false,
|
||||
dmRoomId: RoomId? = null,
|
||||
canCall: Boolean = false,
|
||||
eventSink: (UserProfileEvents) -> Unit = {},
|
||||
) = UserProfileState(
|
||||
userId = userId,
|
||||
@@ -53,5 +57,7 @@ fun aUserProfileState(
|
||||
startDmActionState = startDmActionState,
|
||||
displayConfirmationDialog = displayConfirmationDialog,
|
||||
isCurrentUser = isCurrentUser,
|
||||
dmRoomId = dmRoomId,
|
||||
canCall = canCall,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.userprofile.shared
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
@@ -29,20 +30,14 @@ 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.compound.tokens.generated.CompoundIcons
|
||||
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
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -52,11 +47,13 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
fun UserProfileView(
|
||||
state: UserProfileState,
|
||||
onShareUser: () -> Unit,
|
||||
onDMStarted: (RoomId) -> Unit,
|
||||
onDmStarted: (RoomId) -> Unit,
|
||||
onStartCall: (RoomId) -> Unit,
|
||||
goBack: () -> Unit,
|
||||
openAvatarPreview: (username: String, url: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler { goBack() }
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
@@ -78,12 +75,17 @@ fun UserProfileView(
|
||||
},
|
||||
)
|
||||
|
||||
UserProfileMainActionsSection(onShareUser = onShareUser)
|
||||
UserProfileMainActionsSection(
|
||||
isCurrentUser = state.isCurrentUser,
|
||||
canCall = state.canCall,
|
||||
onShareUser = onShareUser,
|
||||
onStartDM = { state.eventSink(UserProfileEvents.StartDM) },
|
||||
onCall = { state.dmRoomId?.let { onStartCall(it) } }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(26.dp))
|
||||
|
||||
if (!state.isCurrentUser) {
|
||||
StartDMSection(onStartDMClicked = { state.eventSink(UserProfileEvents.StartDM) })
|
||||
BlockUserSection(state)
|
||||
BlockUserDialogs(state)
|
||||
}
|
||||
@@ -94,7 +96,7 @@ fun UserProfileView(
|
||||
progressText = stringResource(CommonStrings.common_starting_chat),
|
||||
)
|
||||
},
|
||||
onSuccess = onDMStarted,
|
||||
onSuccess = onDmStarted,
|
||||
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
|
||||
onRetry = { state.eventSink(UserProfileEvents.StartDM) },
|
||||
onErrorDismiss = { state.eventSink(UserProfileEvents.ClearStartDMState) },
|
||||
@@ -103,18 +105,6 @@ fun UserProfileView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartDMSection(
|
||||
onStartDMClicked: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.common_direct_chat)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chat())),
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = onStartDMClicked,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UserProfileViewPreview(
|
||||
@@ -124,7 +114,8 @@ internal fun UserProfileViewPreview(
|
||||
state = state,
|
||||
onShareUser = {},
|
||||
goBack = {},
|
||||
onDMStarted = {},
|
||||
onDmStarted = {},
|
||||
onStartCall = {},
|
||||
openAvatarPreview = { _, _ -> }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userprofile
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
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.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
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UserProfileViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back key press - the expected callback is called`() = runTest {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setUserProfileView(
|
||||
goBack = callback,
|
||||
)
|
||||
rule.pressBackKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on back button click - the expected callback is called`() = runTest {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setUserProfileView(
|
||||
goBack = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on avatar clicked - the expected callback is called`() = runTest {
|
||||
ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback ->
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL),
|
||||
openAvatarPreview = callback,
|
||||
)
|
||||
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on avatar clicked with no avatar - nothing happens`() = runTest {
|
||||
val callback = EnsureNeverCalledWithTwoParams<String, String>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null),
|
||||
openAvatarPreview = callback,
|
||||
)
|
||||
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Share clicked - the expected callback is called`() = runTest {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setUserProfileView(
|
||||
onShareUser = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_share)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Message clicked - the StartDm event is emitted`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
dmRoomId = A_ROOM_ID,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_message)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.StartDM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Call clicked - the expected callback is called`() = runTest {
|
||||
ensureCalledOnceWithParam(A_ROOM_ID) { callback ->
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
dmRoomId = A_ROOM_ID,
|
||||
canCall = true,
|
||||
),
|
||||
onStartCall = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_call)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_dm_details_block_user)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_dm_details_block_alert_action)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
isBlocked = AsyncData.Success(true),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_dm_details_unblock_user)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
isBlocked = AsyncData.Success(true),
|
||||
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_dm_details_unblock_alert_action)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest {
|
||||
val eventsRecorder = EventsRecorder<UserProfileEvents>()
|
||||
rule.setUserProfileView(
|
||||
state = aUserProfileState(
|
||||
isBlocked = AsyncData.Success(true),
|
||||
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserProfileView(
|
||||
state: UserProfileState = aUserProfileState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
),
|
||||
onShareUser: () -> Unit = EnsureNeverCalled(),
|
||||
onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
) {
|
||||
setContent {
|
||||
UserProfileView(
|
||||
state = state,
|
||||
onShareUser = onShareUser,
|
||||
onDmStarted = onDmStarted,
|
||||
onStartCall = onStartCall,
|
||||
goBack = goBack,
|
||||
openAvatarPreview = openAvatarPreview,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
<string name="action_load_more">"Load more"</string>
|
||||
<string name="action_manage_account">"Manage account"</string>
|
||||
<string name="action_manage_devices">"Manage devices"</string>
|
||||
<string name="action_message">"Message"</string>
|
||||
<string name="action_next">"Next"</string>
|
||||
<string name="action_no">"No"</string>
|
||||
<string name="action_not_now">"Not now"</string>
|
||||
|
||||
@@ -105,3 +105,9 @@ fun <T, R> ensureCalledOnceWithParam(param: T, block: (callback: EnsureCalledOnc
|
||||
block(callback)
|
||||
callback.assertSuccess()
|
||||
}
|
||||
|
||||
fun <P1, P2> ensureCalledOnceWithTwoParams(param1: P1, param2: P2, block: (callback: EnsureCalledOnceWithTwoParams<P1, P2>) -> Unit) {
|
||||
val callback = EnsureCalledOnceWithTwoParams(param1, param2)
|
||||
block(callback)
|
||||
callback.assertSuccess()
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user