Merge pull request #4233 from element-hq/feature/bma/dmCreationConfirmation
Display a bottom sheet to let user confirm the DM creation
This commit is contained in:
commit
fc96c00194
36 changed files with 638 additions and 86 deletions
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.api
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class ConfirmingStartDmWithMatrixUser(
|
||||
val matrixUser: MatrixUser,
|
||||
) : AsyncAction.Confirming
|
||||
|
|
@ -10,13 +10,19 @@ package io.element.android.features.createroom.api
|
|||
import androidx.compose.runtime.MutableState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
interface StartDMAction {
|
||||
/**
|
||||
* Try to find an existing DM with the given user, or create one if none exists.
|
||||
* @param userId The user to start a DM with.
|
||||
* @param matrixUser The user to start a DM with.
|
||||
* @param createIfDmDoesNotExist If true, create a DM if one does not exist. If false and the DM
|
||||
* does not exist, the action will fail with the value [ConfirmingStartDmWithMatrixUser].
|
||||
* @param actionState The state to update with the result of the action.
|
||||
*/
|
||||
suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>)
|
||||
suspend fun execute(
|
||||
matrixUser: MatrixUser,
|
||||
createIfDmDoesNotExist: Boolean,
|
||||
actionState: MutableState<AsyncAction<RoomId>>,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,15 @@ package io.element.android.features.createroom.impl
|
|||
import androidx.compose.runtime.MutableState
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.StartDMResult
|
||||
import io.element.android.libraries.matrix.api.room.startDM
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -26,9 +27,13 @@ class DefaultStartDMAction @Inject constructor(
|
|||
private val matrixClient: MatrixClient,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : StartDMAction {
|
||||
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
|
||||
override suspend fun execute(
|
||||
matrixUser: MatrixUser,
|
||||
createIfDmDoesNotExist: Boolean,
|
||||
actionState: MutableState<AsyncAction<RoomId>>,
|
||||
) {
|
||||
actionState.value = AsyncAction.Loading
|
||||
when (val result = matrixClient.startDM(userId)) {
|
||||
when (val result = matrixClient.startDM(matrixUser.userId, createIfDmDoesNotExist)) {
|
||||
is StartDMResult.Success -> {
|
||||
if (result.isNew) {
|
||||
analyticsService.capture(CreatedRoom(isDM = true))
|
||||
|
|
@ -38,6 +43,9 @@ class DefaultStartDMAction @Inject constructor(
|
|||
is StartDMResult.Failure -> {
|
||||
actionState.value = AsyncAction.Failure(result.throwable)
|
||||
}
|
||||
StartDMResult.DmDoesNotExist -> {
|
||||
actionState.value = ConfirmingStartDmWithMatrixUser(matrixUser = matrixUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ class CreateRoomRootPresenter @Inject constructor(
|
|||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
|
||||
startDMAction.execute(event.matrixUser.userId, startDmActionState)
|
||||
startDMAction.execute(
|
||||
matrixUser = event.matrixUser,
|
||||
createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
|
||||
actionState = startDmActionState,
|
||||
)
|
||||
}
|
||||
CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
|
||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
|
|
@ -49,6 +50,9 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
recentDirectRooms = aRecentDirectRoomList()
|
||||
)
|
||||
),
|
||||
aCreateRoomRootState(
|
||||
startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.createroom.impl.components.UserListView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
|
|
@ -43,6 +44,7 @@ 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.matrix.ui.components.CreateDmConfirmationBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -110,6 +112,19 @@ fun CreateRoomRootView(
|
|||
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
},
|
||||
onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
||||
confirmationDialog = { data ->
|
||||
if (data is ConfirmingStartDmWithMatrixUser) {
|
||||
CreateDmConfirmationBottomSheet(
|
||||
matrixUser = data.matrixUser,
|
||||
onSendInvite = {
|
||||
state.eventSink(CreateRoomRootEvents.StartDM(data.matrixUser))
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ package io.element.android.features.createroom.impl
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -28,10 +29,12 @@ class DefaultStartDMActionTest {
|
|||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenFindDmResult(A_ROOM_ID)
|
||||
}
|
||||
val action = createStartDMAction(matrixClient)
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val action = createStartDMAction(matrixClient, analyticsService)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
action.execute(A_USER_ID, state)
|
||||
action.execute(aMatrixUser(), true, state)
|
||||
assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID))
|
||||
assertThat(analyticsService.capturedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -43,21 +46,38 @@ class DefaultStartDMActionTest {
|
|||
val analyticsService = FakeAnalyticsService()
|
||||
val action = createStartDMAction(matrixClient, analyticsService)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
action.execute(A_USER_ID, state)
|
||||
action.execute(aMatrixUser(), true, state)
|
||||
assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID))
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(CreatedRoom(isDM = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when dm is not found, and createIfDmDoesNotExist is false, assert dm is not created and state is updated to confirmation state`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenFindDmResult(null)
|
||||
givenCreateDmResult(Result.success(A_ROOM_ID))
|
||||
}
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val action = createStartDMAction(matrixClient, analyticsService)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
val matrixUser = aMatrixUser()
|
||||
action.execute(matrixUser, false, state)
|
||||
assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser))
|
||||
assertThat(analyticsService.capturedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when dm creation fails, assert state is updated with given error`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenFindDmResult(null)
|
||||
givenCreateDmResult(Result.failure(A_THROWABLE))
|
||||
}
|
||||
val action = createStartDMAction(matrixClient)
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val action = createStartDMAction(matrixClient, analyticsService)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
action.execute(A_USER_ID, state)
|
||||
action.execute(aMatrixUser(), true, state)
|
||||
assertThat(state.value).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
assertThat(analyticsService.capturedEvents).isEmpty()
|
||||
}
|
||||
|
||||
private fun createStartDMAction(
|
||||
|
|
|
|||
|
|
@ -7,16 +7,19 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -24,6 +27,9 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
|||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -33,46 +39,130 @@ class CreateRoomRootPresenterTest {
|
|||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - start DM action complete scenario`() = runTest {
|
||||
val startDMAction = FakeStartDMAction()
|
||||
fun `present - start DM action failure scenario`() = runTest {
|
||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMFailureResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createCreateRoomRootPresenter(startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName)
|
||||
assertThat(initialState.userListState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
|
||||
|
||||
val matrixUser = MatrixUser(UserId("@name:domain"))
|
||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
||||
|
||||
// Failure
|
||||
startDMAction.givenExecuteResult(startDMFailureResult)
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmAction).isEqualTo(startDMFailureResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
}
|
||||
|
||||
// Success
|
||||
startDMAction.givenExecuteResult(startDMSuccessResult)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
state.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(state.startDmAction.isUninitialized()).isTrue()
|
||||
}
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action success scenario`() = runTest {
|
||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMSuccessResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createCreateRoomRootPresenter(startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName)
|
||||
assertThat(initialState.userListState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
|
||||
val matrixUser = MatrixUser(UserId("@name:domain"))
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmAction).isEqualTo(startDMSuccessResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action confirmation scenario - cancel`() = runTest {
|
||||
val matrixUser = MatrixUser(UserId("@name:domain"))
|
||||
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMConfirmationResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createCreateRoomRootPresenter(startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
// Cancelling should not create the DM
|
||||
confirmingState.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.startDmAction.isUninitialized()).isTrue()
|
||||
executeResult.assertions().isCalledExactly(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action confirmation scenario - confirm`() = runTest {
|
||||
val matrixUser = MatrixUser(UserId("@name:domain"))
|
||||
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMConfirmationResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createCreateRoomRootPresenter(startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
// Start DM again should invoke the action with createIfDmDoesNotExist = true
|
||||
confirmingState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
executeResult.assertions().isCalledExactly(2).withSequence(
|
||||
listOf(value(matrixUser), value(false), any()),
|
||||
listOf(value(matrixUser), value(true), any()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCreateRoomRootPresenter(
|
||||
startDMAction: StartDMAction = FakeStartDMAction(),
|
||||
): CreateRoomRootPresenter {
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrix.test)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.tests.testutils)
|
||||
api(projects.features.createroom.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,20 +11,19 @@ import androidx.compose.runtime.MutableState
|
|||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import kotlinx.coroutines.delay
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeStartDMAction : StartDMAction {
|
||||
private var executeResult: AsyncAction<RoomId> = AsyncAction.Success(A_ROOM_ID)
|
||||
|
||||
fun givenExecuteResult(result: AsyncAction<RoomId>) {
|
||||
executeResult = result
|
||||
class FakeStartDMAction(
|
||||
private val executeResult: (MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>) -> Unit = { _, _, _ ->
|
||||
lambdaError()
|
||||
}
|
||||
|
||||
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
|
||||
actionState.value = AsyncAction.Loading
|
||||
delay(1)
|
||||
actionState.value = executeResult
|
||||
) : StartDMAction {
|
||||
override suspend fun execute(
|
||||
matrixUser: MatrixUser,
|
||||
createIfDmDoesNotExist: Boolean,
|
||||
actionState: MutableState<AsyncAction<RoomId>>,
|
||||
) {
|
||||
executeResult(matrixUser, createIfDmDoesNotExist, actionState)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,11 @@ class UserProfilePresenter @AssistedInject constructor(
|
|||
}
|
||||
UserProfileEvents.StartDM -> {
|
||||
coroutineScope.launch {
|
||||
startDMAction.execute(userId, startDmActionState)
|
||||
startDMAction.execute(
|
||||
matrixUser = userProfile ?: MatrixUser(userId),
|
||||
createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
|
||||
actionState = startDmActionState,
|
||||
)
|
||||
}
|
||||
}
|
||||
UserProfileEvents.ClearStartDMState -> {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@
|
|||
|
||||
package io.element.android.features.userprofile.impl
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
|
|
@ -19,6 +24,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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
|
||||
|
|
@ -30,6 +36,9 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
|||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -229,37 +238,122 @@ class UserProfilePresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action complete scenario`() = runTest {
|
||||
val startDMAction = FakeStartDMAction()
|
||||
fun `present - start DM action failure scenario`() = runTest {
|
||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMFailureResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
|
||||
presenter.test {
|
||||
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)
|
||||
val matrixUser = MatrixUser(UserId("@alice:server.org"))
|
||||
initialState.eventSink(UserProfileEvents.StartDM)
|
||||
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
state.eventSink(UserProfileEvents.ClearStartDMState)
|
||||
}
|
||||
|
||||
// Success
|
||||
startDMAction.givenExecuteResult(startDMSuccessResult)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized)
|
||||
state.eventSink(UserProfileEvents.StartDM)
|
||||
assertThat(state.startDmActionState.isUninitialized()).isTrue()
|
||||
}
|
||||
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action success scenario`() = runTest {
|
||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMSuccessResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
val matrixUser = MatrixUser(UserId("@alice:server.org"))
|
||||
initialState.eventSink(UserProfileEvents.StartDM)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action confirmation scenario - cancel`() = runTest {
|
||||
val matrixUser = MatrixUser(UserId("@alice:server.org"))
|
||||
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMConfirmationResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
initialState.eventSink(UserProfileEvents.StartDM)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
// Cancelling should not create the DM
|
||||
confirmingState.eventSink(UserProfileEvents.ClearStartDMState)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.startDmActionState.isUninitialized()).isTrue()
|
||||
executeResult.assertions().isCalledExactly(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - start DM action confirmation scenario - confirm`() = runTest {
|
||||
val matrixUser = MatrixUser(UserId("@alice:server.org"))
|
||||
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
|
||||
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
|
||||
actionState.value = startDMConfirmationResult
|
||||
}
|
||||
val startDMAction = FakeStartDMAction(executeResult = executeResult)
|
||||
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
initialState.eventSink(UserProfileEvents.StartDM)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult)
|
||||
executeResult.assertions().isCalledOnce().with(
|
||||
value(matrixUser),
|
||||
value(false),
|
||||
any(),
|
||||
)
|
||||
// Start DM again should invoke the action with createIfDmDoesNotExist = true
|
||||
confirmingState.eventSink(UserProfileEvents.StartDM)
|
||||
executeResult.assertions().isCalledExactly(2).withSequence(
|
||||
listOf(value(matrixUser), value(false), any()),
|
||||
listOf(value(matrixUser), value(true), any()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when user is verified, the value in the state is true`() = runTest {
|
||||
val client = createFakeMatrixClient(isUserVerified = true)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
package io.element.android.features.userprofile.shared
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.userprofile.api.UserProfileEvents
|
||||
import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
||||
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
|
||||
override val values: Sequence<UserProfileState>
|
||||
|
|
@ -26,7 +28,7 @@ open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState>
|
|||
aUserProfileState(isBlocked = AsyncData.Loading(true), isVerified = AsyncData.Loading()),
|
||||
aUserProfileState(startDmActionState = AsyncAction.Loading),
|
||||
aUserProfileState(canCall = true),
|
||||
// Add other states here
|
||||
aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser())),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ 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.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
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
|
||||
|
|
@ -37,6 +38,7 @@ 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.matrix.ui.components.CreateDmConfirmationBottomSheet
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -95,6 +97,19 @@ fun UserProfileView(
|
|||
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
|
||||
onRetry = { state.eventSink(UserProfileEvents.StartDM) },
|
||||
onErrorDismiss = { state.eventSink(UserProfileEvents.ClearStartDMState) },
|
||||
confirmationDialog = { data ->
|
||||
if (data is ConfirmingStartDmWithMatrixUser) {
|
||||
CreateDmConfirmationBottomSheet(
|
||||
matrixUser = data.matrixUser,
|
||||
onSendInvite = {
|
||||
state.eventSink(UserProfileEvents.StartDM)
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(UserProfileEvents.ClearStartDMState)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,4 +59,6 @@ enum class AvatarSize(val dp: Dp) {
|
|||
KnockRequestBanner(32.dp),
|
||||
|
||||
MediaSender(32.dp),
|
||||
|
||||
DmCreationConfirmation(64.dp),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,21 +12,27 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
/**
|
||||
* Try to find an existing DM with the given user, or create one if none exists.
|
||||
* Try to find an existing DM with the given user, or create one if none exists and [createIfDmDoesNotExist] is true.
|
||||
*/
|
||||
suspend fun MatrixClient.startDM(userId: UserId): StartDMResult {
|
||||
suspend fun MatrixClient.startDM(
|
||||
userId: UserId,
|
||||
createIfDmDoesNotExist: Boolean,
|
||||
): StartDMResult {
|
||||
val existingDM = findDM(userId)
|
||||
return if (existingDM != null) {
|
||||
StartDMResult.Success(existingDM, isNew = false)
|
||||
} else {
|
||||
} else if (createIfDmDoesNotExist) {
|
||||
createDM(userId).fold(
|
||||
{ StartDMResult.Success(it, isNew = true) },
|
||||
{ StartDMResult.Failure(it) }
|
||||
)
|
||||
} else {
|
||||
StartDMResult.DmDoesNotExist
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface StartDMResult {
|
||||
data class Success(val roomId: RoomId, val isNew: Boolean) : StartDMResult
|
||||
data object DmDoesNotExist : StartDMResult
|
||||
data class Failure(val throwable: Throwable) : StartDMResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.R
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getFullName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Figma:
|
||||
* https://www.figma.com/design/dywzKQvHYxFD1Ncn4a5NkI/PSB-675%253A-Improve-invite-into-a-DM?node-id=12-36886
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreateDmConfirmationBottomSheet(
|
||||
matrixUser: MatrixUser,
|
||||
onSendInvite: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Avatar(
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_bottom_sheet_create_dm_title),
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onSendInvite,
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()),
|
||||
text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onDismiss,
|
||||
text = stringResource(CommonStrings.action_cancel),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview {
|
||||
CreateDmConfirmationBottomSheet(
|
||||
matrixUser = matrixUser,
|
||||
onSendInvite = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
|||
|
||||
fun aMatrixUser(
|
||||
id: String = "@id_of_alice:server.org",
|
||||
displayName: String = "Alice",
|
||||
displayName: String? = "Alice",
|
||||
avatarUrl: String? = null,
|
||||
) = MatrixUser(
|
||||
userId = UserId(id),
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.model
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData(
|
||||
id = userId.value,
|
||||
|
|
@ -21,3 +24,14 @@ fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData(
|
|||
fun MatrixUser.getBestName(): String {
|
||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixUser.getFullName(): String {
|
||||
return displayName.let { name ->
|
||||
if (name.isNullOrBlank()) {
|
||||
userId.value
|
||||
} else {
|
||||
stringResource(CommonStrings.common_name_and_id, name, userId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Send invite"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Would you like to start a chat with %1$s?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Send invite?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.messages.reply
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
|
|
@ -52,6 +46,7 @@ import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
|
|||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
import io.element.android.tests.testutils.withConfigurationAndContext
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -350,7 +345,7 @@ class InReplyToMetadataKtTest {
|
|||
@Test
|
||||
fun `a location message content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
testEnv {
|
||||
withConfigurationAndContext {
|
||||
anInReplyToDetailsReady(
|
||||
eventContent = aMessageContent(
|
||||
messageType = LocationMessageType(
|
||||
|
|
@ -380,7 +375,7 @@ class InReplyToMetadataKtTest {
|
|||
@Test
|
||||
fun `a voice message content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
testEnv {
|
||||
withConfigurationAndContext {
|
||||
anInReplyToDetailsReady(
|
||||
eventContent = aMessageContent(
|
||||
messageType = VoiceMessageType(
|
||||
|
|
@ -588,17 +583,3 @@ fun anImageInfo(): ImageInfo {
|
|||
blurhash = A_BLUR_HASH,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun testEnv(content: @Composable () -> Any?): Any? {
|
||||
var result: Any? = null
|
||||
CompositionLocalProvider(
|
||||
LocalConfiguration provides Configuration(),
|
||||
LocalContext provides ApplicationProvider.getApplicationContext(),
|
||||
) {
|
||||
content().apply {
|
||||
result = this
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.model
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.withConfigurationAndContext
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MatrixUserExtensionsTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `getAvatarData should return the expected value`() {
|
||||
val matrixUser = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "displayName",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
val expected = AvatarData(
|
||||
id = A_USER_ID.value,
|
||||
name = "displayName",
|
||||
url = "avatarUrl",
|
||||
size = AvatarSize.UserHeader,
|
||||
)
|
||||
assertThat(matrixUser.getAvatarData(AvatarSize.UserHeader)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBestName should return the display name is available`() {
|
||||
val matrixUser = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "displayName",
|
||||
)
|
||||
assertThat(matrixUser.getBestName()).isEqualTo("displayName")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBestName should return the id when name is not available`() {
|
||||
val matrixUser = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = null,
|
||||
)
|
||||
assertThat(matrixUser.getBestName()).isEqualTo(A_USER_ID.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBestName should return the id when name is empty`() {
|
||||
val matrixUser = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "",
|
||||
)
|
||||
assertThat(matrixUser.getBestName()).isEqualTo(A_USER_ID.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFullName should return the display name is available and the userId`() = runTest {
|
||||
val matrixUser = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "displayName",
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
withConfigurationAndContext {
|
||||
matrixUser.getFullName()
|
||||
}
|
||||
}.test {
|
||||
assertThat(awaitItem()).isEqualTo("displayName (@alice:server.org)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBestName should return only the id when name is not available`() = runTest {
|
||||
val matrixUser = MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = null,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
matrixUser.getFullName()
|
||||
}.test {
|
||||
assertThat(awaitItem()).isEqualTo(A_USER_ID.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -193,6 +193,7 @@ Reason: %1$s."</string>
|
|||
<string name="common_message_removed">"Message removed"</string>
|
||||
<string name="common_modern">"Modern"</string>
|
||||
<string name="common_mute">"Mute"</string>
|
||||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||
<string name="common_no_results">"No results"</string>
|
||||
<string name="common_no_room_name">"No room name"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.testutils
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
@Composable
|
||||
fun withConfigurationAndContext(content: @Composable () -> Any?): Any? {
|
||||
var result: Any? = null
|
||||
CompositionLocalProvider(
|
||||
LocalConfiguration provides Configuration(),
|
||||
LocalContext provides ApplicationProvider.getApplicationContext(),
|
||||
) {
|
||||
result = content()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1ced96a86269f22fad7d93a27bc0c302ce63f45857b4f15f764a8f04fe0d96b6
|
||||
size 41925
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:13019305b802fb72dc92e3ee68f581f2fa93e122d8e8d6f5fd1311869535ef2e
|
||||
size 40334
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dbd7611e8df66b7e691c3faa78ada45a186f004a5676ac276c35c5de86e72d0a
|
||||
size 34954
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4dc1b2fcd2aca5b0180e3551d2bad43e50927f154e3b21e305046d9b9abedb49
|
||||
size 33472
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1104fc4c6acc07da2aa9fcc7e15a95a3ba1263f7bf976040af068724ec7caee6
|
||||
size 20708
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b2249dc7c9627f50d8895208c79e22760aa2bc484939bfc648f2fbcffc20bbca
|
||||
size 19239
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fd50a626209cc9b66c8e4847a0ba4e96e39ed2638235fc6e50b56b0d239961b
|
||||
size 24405
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c0fa36e2715a13dc388a9a7446c233ba0b5d03f8bd4861ac32f776164545409e
|
||||
size 26128
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b16ac7e4ea4e1e5d34d301f848f0f819df813204994ef1ec8db23402dbfefb22
|
||||
size 24305
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bcbe93a450fd92eec005ac15e526c2a3973dc841129a14cb32505d173f6b24db
|
||||
size 25031
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29a91d04c8f4e10a8cc5f90a0ec4d0b42b9461e427a19873c429cd3589cd2768
|
||||
size 23285
|
||||
|
|
@ -273,7 +273,8 @@
|
|||
{
|
||||
"name" : ":libraries:matrixui",
|
||||
"includeRegex" : [
|
||||
"screen_invites_invited_you"
|
||||
"screen_invites_invited_you",
|
||||
"screen\\.bottom_sheet\\.create_dm\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue