From 44e2e247332329458aa85dc67766c1faf8824f09 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 24 Mar 2023 16:01:14 +0100 Subject: [PATCH 01/19] Create or retrieve DM --- .../android/appnav/LoggedInFlowNode.kt | 12 ++++- features/createroom/api/build.gradle.kts | 1 + .../createroom/api/CreateRoomEntryPoint.kt | 19 +++++++- .../createroom/impl/CreateRoomFlowNode.kt | 7 +++ .../impl/DefaultCreateRoomEntryPoint.kt | 19 +++++++- .../impl/root/CreateRoomRootEvents.kt | 4 +- .../impl/root/CreateRoomRootNode.kt | 20 ++++---- .../impl/root/CreateRoomRootPresenter.kt | 46 +++++++++++++++++-- .../impl/root/CreateRoomRootState.kt | 4 ++ .../impl/root/CreateRoomRootStateProvider.kt | 5 +- .../impl/root/CreateRoomRootView.kt | 41 ++++++++++++++++- .../impl/root/CreateRoomRootPresenterTests.kt | 9 ++-- .../selectusers/api/SelectUsersEvents.kt | 1 + .../impl/DefaultSelectUsersPresenter.kt | 1 + .../libraries/matrix/api/MatrixClient.kt | 3 ++ .../libraries/matrix/impl/RustMatrixClient.kt | 27 +++++++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 29 ++++++++++-- 17 files changed, 220 insertions(+), 28 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 4b2d653d79..fa2643b4ff 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -32,6 +32,7 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.operation.replace import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -178,7 +179,16 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } NavTarget.CreateRoom -> { - createRoomEntryPoint.createNode(this, buildContext) + val callback = object : CreateRoomEntryPoint.Callback { + override fun onOpenRoom(roomId: RoomId) { + backstack.replace(NavTarget.Room(roomId)) + } + } + + createRoomEntryPoint + .nodeBuilder(this, buildContext) + .callback(callback) + .build() } NavTarget.VerifySession -> { verifySessionEntryPoint.createNode(this, buildContext) diff --git a/features/createroom/api/build.gradle.kts b/features/createroom/api/build.gradle.kts index b3fceedc27..cafcf7c4b5 100644 --- a/features/createroom/api/build.gradle.kts +++ b/features/createroom/api/build.gradle.kts @@ -24,4 +24,5 @@ android { dependencies { implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) } diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt index 049c101806..73f5110daa 100644 --- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt @@ -16,6 +16,21 @@ package io.element.android.features.createroom.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId -interface CreateRoomEntryPoint : SimpleFeatureEntryPoint +interface CreateRoomEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 13c3ff52be..22851904c4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -23,17 +23,20 @@ import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack 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.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -64,6 +67,10 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } + + override fun onOpenRoom(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId) } + } } createNode(buildContext, plugins = listOf(callback)) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt index 214ed3a9ec..34e514be3e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultCreateRoomEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,21 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder { + + val plugins = ArrayList() + + return object : CreateRoomEntryPoint.NodeBuilder { + + override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index 5d2f0f684c..d90708ed2b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -19,6 +19,8 @@ package io.element.android.features.createroom.impl.root import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { - data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents object InvitePeople : CreateRoomRootEvents + data class SelectUser(val matrixUser: MatrixUser) : CreateRoomRootEvents + data class CreateDM(val matrixUser: MatrixUser) : CreateRoomRootEvents + object CancelCreateDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt index dadc16efc4..3a714a0eb3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt @@ -16,7 +16,6 @@ package io.element.android.features.createroom.impl.root -import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext @@ -27,7 +26,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope -import kotlinx.parcelize.Parcelize +import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) class CreateRoomRootNode @AssistedInject constructor( @@ -38,15 +37,17 @@ class CreateRoomRootNode @AssistedInject constructor( interface Callback : Plugin { fun onCreateNewRoom() + fun onOpenRoom(roomId: RoomId) } - private fun onCreateNewRoom() { - plugins().forEach { it.onCreateNewRoom() } - } + private val callback = object : Callback { + override fun onCreateNewRoom() { + plugins().forEach { it.onCreateNewRoom() } + } - sealed interface NavTarget : Parcelable { - @Parcelize - object Root : NavTarget + override fun onOpenRoom(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId) } + } } @Composable @@ -56,7 +57,8 @@ class CreateRoomRootNode @AssistedInject constructor( state = state, modifier = modifier, onClosePressed = this::navigateUp, - onNewRoomClicked = this::onCreateNewRoom, + onNewRoomClicked = callback::onCreateNewRoom, + onOpenDM = callback::onOpenRoom, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2f3f3ded4a..68901037f0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -17,16 +17,30 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.selectusers.api.SelectUsersEvents import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.execute +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.model.MatrixUser -import timber.log.Timber +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject class CreateRoomRootPresenter @Inject constructor( private val presenterFactory: SelectUsersPresenter.Factory, + private val matrixClient: MatrixClient, ) : Presenter { private val presenter by lazy { @@ -37,20 +51,44 @@ class CreateRoomRootPresenter @Inject constructor( override fun present(): CreateRoomRootState { val selectUsersState = presenter.present() + val localCoroutineScope = rememberCoroutineScope() + + var showCreateDmConfirmationDialog by rememberSaveable { mutableStateOf(false) } + val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.StartDM -> handleStartDM(event.matrixUser) + is CreateRoomRootEvents.SelectUser -> { + val existingDM = matrixClient.findDM(event.matrixUser.id) + if (existingDM == null) { + showCreateDmConfirmationDialog = true + } else { + startDmAction.value = Async.Success(existingDM.roomId) + } + } + is CreateRoomRootEvents.CreateDM -> { + showCreateDmConfirmationDialog = false + localCoroutineScope.createDM(event.matrixUser, startDmAction) + } + CreateRoomRootEvents.CancelCreateDM -> { + showCreateDmConfirmationDialog = false + selectUsersState.eventSink(SelectUsersEvents.ClearSelection) + } CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } return CreateRoomRootState( selectUsersState = selectUsersState, + showCreateDmConfirmationDialog = showCreateDmConfirmationDialog, + startDmAction = startDmAction.value, eventSink = ::handleEvents, ) } - private fun handleStartDM(matrixUser: MatrixUser) { - Timber.d("handleStartDM: $matrixUser") // Todo handle start DM action + private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState>) = launch { + suspend { + matrixClient.createDM(user.id).getOrThrow() + }.execute(startDmAction) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index a57d6aaaf6..89b702a011 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -17,8 +17,12 @@ package io.element.android.features.createroom.impl.root import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId data class CreateRoomRootState( val selectUsersState: SelectUsersState, + val showCreateDmConfirmationDialog: Boolean, + val startDmAction: Async, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 678f02476c..750b9cfe32 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.libraries.architecture.Async open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,5 +29,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider Unit = {}, onNewRoomClicked: () -> Unit = {}, + onOpenDM: (RoomId) -> Unit = {}, ) { + if (state.startDmAction is Async.Success) { + LaunchedEffect(state.startDmAction) { + onOpenDM(state.startDmAction.state) + } + } + Scaffold( modifier = modifier.fillMaxWidth(), topBar = { @@ -72,7 +85,7 @@ fun CreateRoomRootView( SelectUsersView( modifier = Modifier.fillMaxWidth(), state = state.selectUsersState, - onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) }, + onUserSelected = { state.eventSink(CreateRoomRootEvents.SelectUser(it)) }, ) if (!state.selectUsersState.isSearchActive) { @@ -83,6 +96,12 @@ fun CreateRoomRootView( } } } + + CreateDmConfirmationDialog(state) + + if (state.startDmAction is Async.Loading) { + ProgressDialog(text = "Creating room...") + } } @OptIn(ExperimentalMaterial3Api::class) @@ -153,6 +172,26 @@ fun CreateRoomActionButton( } } +@Composable +fun CreateDmConfirmationDialog( + state: CreateRoomRootState, + modifier: Modifier = Modifier, +) { + if (state.showCreateDmConfirmationDialog) { + val selectedUser = state.selectUsersState.selectedUsers.firstOrNull() + if (selectedUser != null) { + ConfirmationDialog( + modifier = modifier, + title = "Start chat", + content = "You're about starting a chat with ${selectedUser.getBestName()}, do you want to continue?", + submitText = stringResource(io.element.android.libraries.ui.strings.R.string._continue), + onSubmitClicked = { state.eventSink(CreateRoomRootEvents.CreateDM(selectedUser)) }, + onDismiss = { state.eventSink(CreateRoomRootEvents.CancelCreateDM) }, + ) + } + } +} + @Preview @Composable fun CreateRoomRootViewLightPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) = diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 5dc9ec00e9..4aa1068ce4 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -25,6 +25,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -34,13 +35,15 @@ import org.junit.Test class CreateRoomRootPresenterTests { private lateinit var presenter: CreateRoomRootPresenter + private lateinit var fakeMatrixClient: FakeMatrixClient @Before fun setup() { val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) } - presenter = CreateRoomRootPresenter(selectUsersPresenter) + fakeMatrixClient = FakeMatrixClient() + presenter = CreateRoomRootPresenter(selectUsersPresenter, fakeMatrixClient) } @Test @@ -64,13 +67,13 @@ class CreateRoomRootPresenterTests { } @Test - fun `present - trigger start DM action`() = runTest { + fun `present - trigger select user action`() = runTest { moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() val matrixUser = MatrixUser(UserId("@name:matrix.org")) - initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + initialState.eventSink(CreateRoomRootEvents.SelectUser(matrixUser)) } } } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt index e0ee6ddf68..22c72f6dfb 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt +++ b/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt @@ -22,5 +22,6 @@ sealed interface SelectUsersEvents { data class UpdateSearchQuery(val query: String) : SelectUsersEvents data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents + object ClearSelection : SelectUsersEvents data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents } diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt index e1135cd1a2..55c2cd694d 100644 --- a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt +++ b/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt @@ -79,6 +79,7 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) } is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + SelectUsersEvents.ClearSelection -> selectedUsers.value = persistentListOf() } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 9ce5502843..15ba838bcb 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource @@ -28,6 +29,8 @@ interface MatrixClient : Closeable { val sessionId: SessionId val roomSummaryDataSource: RoomSummaryDataSource fun getRoom(roomId: RoomId): MatrixRoom? + suspend fun createDM(userId: UserId): Result + fun findDM(userId: UserId): MatrixRoom? fun startSync() fun stopSync() fun mediaResolver(): MediaResolver diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 378fec56c7..ce9286d2be 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -37,7 +37,10 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.CreateRoomParameters import org.matrix.rustcomponents.sdk.RequiredState +import org.matrix.rustcomponents.sdk.RoomPreset +import org.matrix.rustcomponents.sdk.RoomVisibility import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters @@ -154,6 +157,30 @@ class RustMatrixClient constructor( ) } + override fun findDM(userId: UserId): MatrixRoom? { + val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) } + return roomId?.let { getRoom(it) } + } + + override suspend fun createDM(userId: UserId): Result = + withContext(dispatchers.io) { + runCatching { + val roomId = client.createRoom( + CreateRoomParameters( + name = "", + topic = null, + isEncrypted = true, + isDirect = true, + visibility = RoomVisibility.PRIVATE, + preset = RoomPreset.TRUSTED_PRIVATE_CHAT, + invite = listOf(userId.value), + avatar = null, + ) + ) + RoomId(roomId) + } + } + override fun mediaResolver(): MediaResolver = mediaResolver override fun sessionVerificationService(): SessionVerificationService = verificationService diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 9272e794b4..ec5068e93d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test 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.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource @@ -37,12 +38,22 @@ class FakeMatrixClient( private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService() ) : MatrixClient { + private var createDmResult: Result = Result.success(A_ROOM_ID) + private var findDmResult: MatrixRoom? = FakeMatrixRoom() private var logoutFailure: Throwable? = null override fun getRoom(roomId: RoomId): MatrixRoom? { return FakeMatrixRoom(roomId) } + override suspend fun createDM(userId: UserId): Result { + return createDmResult + } + + override fun findDM(userId: UserId): MatrixRoom? { + return findDmResult + } + override fun startSync() = Unit override fun stopSync() = Unit @@ -51,10 +62,6 @@ class FakeMatrixClient( return FakeMediaResolver() } - fun givenLogoutError(failure: Throwable) { - logoutFailure = failure - } - override suspend fun logout() { delay(100) logoutFailure?.let { throw it } @@ -81,4 +88,18 @@ class FakeMatrixClient( override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun onSlidingSyncUpdate() {} + + // Mocks + + fun givenLogoutError(failure: Throwable) { + logoutFailure = failure + } + + fun givenCreateDmResult(result: Result) { + createDmResult = result + } + + fun givenFindDmResult(result: MatrixRoom?) { + findDmResult = result + } } From 7e3b69f06b8e1c15a5c28fafc0f4ef9f6a55a7d0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 07:41:04 +0200 Subject: [PATCH 02/19] convert rustsdk gradle file to kts --- libraries/rustsdk/build.gradle | 2 -- libraries/rustsdk/build.gradle.kts | 2 ++ settings.gradle.kts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 libraries/rustsdk/build.gradle create mode 100644 libraries/rustsdk/build.gradle.kts diff --git a/libraries/rustsdk/build.gradle b/libraries/rustsdk/build.gradle deleted file mode 100644 index bfafe67f28..0000000000 --- a/libraries/rustsdk/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('matrix-rust-sdk.aar')) \ No newline at end of file diff --git a/libraries/rustsdk/build.gradle.kts b/libraries/rustsdk/build.gradle.kts new file mode 100644 index 0000000000..56fd094897 --- /dev/null +++ b/libraries/rustsdk/build.gradle.kts @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file("matrix-rust-sdk.aar")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 944b17ab52..e44a2cb8fa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,7 +41,6 @@ include(":appnav") include(":tests:uitests") include(":anvilannotations") include(":anvilcodegen") -include(":libraries:rustsdk") include(":samples:minimal") From 7ac828de5231446987cd991a5bd5becc902d3867 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 07:41:44 +0200 Subject: [PATCH 03/19] fix rebase issue --- .../android/features/createroom/impl/root/CreateRoomRootView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 8c0e9382d0..6259840a21 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -184,7 +184,7 @@ fun CreateDmConfirmationDialog( modifier = modifier, title = "Start chat", content = "You're about starting a chat with ${selectedUser.getBestName()}, do you want to continue?", - submitText = stringResource(io.element.android.libraries.ui.strings.R.string._continue), + submitText = stringResource(io.element.android.libraries.ui.strings.R.string.action_continue), onSubmitClicked = { state.eventSink(CreateRoomRootEvents.CreateDM(selectedUser)) }, onDismiss = { state.eventSink(CreateRoomRootEvents.CancelCreateDM) }, ) From ec323051fed201a641b15a1806155b62a923fb7b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 08:47:06 +0200 Subject: [PATCH 04/19] Add preview --- .../impl/root/CreateRoomRootStateProvider.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 750b9cfe32..be720de5b2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -19,11 +19,24 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.selectusers.api.aSelectUsersState import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.collections.immutable.persistentListOf open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreateRoomRootState(), + aCreateRoomRootState().copy( + showCreateDmConfirmationDialog = true, + selectUsersState = aMatrixUser().let { + aSelectUsersState().copy( + searchQuery = it.id.value, + searchResults = persistentListOf(it), + selectedUsers = persistentListOf(it), + isSearchActive = true, + ) + } + ), ) } From 55dc2c5da8617a5a70ec026e5e16620cac409012 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 09:34:13 +0200 Subject: [PATCH 05/19] Remove confirmation dialog --- .../impl/root/CreateRoomRootEvents.kt | 4 +-- .../impl/root/CreateRoomRootPresenter.kt | 19 ++------------ .../impl/root/CreateRoomRootState.kt | 1 - .../impl/root/CreateRoomRootStateProvider.kt | 5 ++-- .../impl/root/CreateRoomRootView.kt | 26 +------------------ .../impl/root/CreateRoomRootPresenterTests.kt | 2 +- .../selectusers/api/SelectUsersEvents.kt | 1 - .../impl/DefaultSelectUsersPresenter.kt | 1 - 8 files changed, 7 insertions(+), 52 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index d90708ed2b..d3cd1e0287 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -20,7 +20,5 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { object InvitePeople : CreateRoomRootEvents - data class SelectUser(val matrixUser: MatrixUser) : CreateRoomRootEvents - data class CreateDM(val matrixUser: MatrixUser) : CreateRoomRootEvents - object CancelCreateDM : CreateRoomRootEvents + data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 68901037f0..2ec32c7bac 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -18,13 +18,9 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import io.element.android.features.selectusers.api.SelectUsersEvents import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.api.SelectionMode @@ -52,35 +48,24 @@ class CreateRoomRootPresenter @Inject constructor( val selectUsersState = presenter.present() val localCoroutineScope = rememberCoroutineScope() - - var showCreateDmConfirmationDialog by rememberSaveable { mutableStateOf(false) } val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.SelectUser -> { + is CreateRoomRootEvents.StartDM -> { val existingDM = matrixClient.findDM(event.matrixUser.id) if (existingDM == null) { - showCreateDmConfirmationDialog = true + localCoroutineScope.createDM(event.matrixUser, startDmAction) } else { startDmAction.value = Async.Success(existingDM.roomId) } } - is CreateRoomRootEvents.CreateDM -> { - showCreateDmConfirmationDialog = false - localCoroutineScope.createDM(event.matrixUser, startDmAction) - } - CreateRoomRootEvents.CancelCreateDM -> { - showCreateDmConfirmationDialog = false - selectUsersState.eventSink(SelectUsersEvents.ClearSelection) - } CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } return CreateRoomRootState( selectUsersState = selectUsersState, - showCreateDmConfirmationDialog = showCreateDmConfirmationDialog, startDmAction = startDmAction.value, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index 89b702a011..9b8b81aef1 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.RoomId data class CreateRoomRootState( val selectUsersState: SelectUsersState, - val showCreateDmConfirmationDialog: Boolean, val startDmAction: Async, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index be720de5b2..e0e3152318 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -27,7 +27,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() - SelectUsersEvents.ClearSelection -> selectedUsers.value = persistentListOf() } } From b82ee668550fa6ae3c2a7bd72d8e0dfc79959c31 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 10:01:04 +0200 Subject: [PATCH 06/19] Add presenter tests --- .../impl/root/CreateRoomRootPresenterTests.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 1cca23f74a..c3cacb25c7 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -24,8 +24,11 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.selectusers.api.SelectUsersPresenterArgs import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter +import io.element.android.libraries.architecture.Async +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.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -67,13 +70,39 @@ class CreateRoomRootPresenterTests { } @Test - fun `present - trigger select user action`() = runTest { + fun `present - trigger create DM action`() = runTest { moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() val matrixUser = MatrixUser(UserId("@name:matrix.org")) + val createDmResult = Result.success(RoomId("!createDmResult")) + + fakeMatrixClient.givenFindDmResult(null) + fakeMatrixClient.givenCreateDmResult(createDmResult) + + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + val stateAfterStartDM = awaitItem() + assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) + assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) + } + } + + @Test + fun `present - trigger retrieve DM action`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val matrixUser = MatrixUser(UserId("@name:matrix.org")) + val fakeDmResult = FakeMatrixRoom(RoomId("!fakeDmResult")) + + fakeMatrixClient.givenFindDmResult(fakeDmResult) + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + val stateAfterStartDM = awaitItem() + assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) + assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId) } } } From 014d14ff79c30d1135363d758ab2a677669cf1ee Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 10:13:36 +0200 Subject: [PATCH 07/19] Use string resource --- .../android/features/createroom/impl/root/CreateRoomRootView.kt | 2 +- libraries/ui-strings/src/main/res/values/localazy.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index b75bddc8e0..4266878a33 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -96,7 +96,7 @@ fun CreateRoomRootView( } if (state.startDmAction is Async.Loading) { - ProgressDialog(text = "Creating room...") + ProgressDialog(text = stringResource(id = StringR.string.common_creating_room)) } } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index a28d8bb7bc..084a442442 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -48,6 +48,7 @@ "About" "Audio" "Bubbles" + "Creating room…" "Decryption error" "Developer options" "(edited)" From 6ab128ba4d16695eda99bb64838c281dd47e5b0c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 10:51:39 +0200 Subject: [PATCH 08/19] Changelog --- changelog.d/96.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/96.feature diff --git a/changelog.d/96.feature b/changelog.d/96.feature new file mode 100644 index 0000000000..7a1c8d21b8 --- /dev/null +++ b/changelog.d/96.feature @@ -0,0 +1 @@ +[Create and join rooms] Show or create direct message room From 36afd71c2993dc88b2c90bfb1d723c12b89550d0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 13:48:54 +0200 Subject: [PATCH 09/19] Handle errors on create DM --- .../impl/root/CreateRoomRootEvents.kt | 2 + .../impl/root/CreateRoomRootPresenter.kt | 22 ++++-- .../impl/root/CreateRoomRootStateProvider.kt | 13 +++- .../impl/root/CreateRoomRootView.kt | 17 ++++- .../impl/root/CreateRoomRootPresenterTests.kt | 72 +++++++++++++++++-- .../components/dialogs/ErrorDialog.kt | 11 ++- .../libraries/matrix/test/FakeMatrixClient.kt | 9 ++- 7 files changed, 130 insertions(+), 16 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index d3cd1e0287..de9810f34a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -21,4 +21,6 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { object InvitePeople : CreateRoomRootEvents data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents + object RetryStartDM : CreateRoomRootEvents + object CancelStartDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2ec32c7bac..6bc8d3c45e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -50,16 +50,24 @@ class CreateRoomRootPresenter @Inject constructor( val localCoroutineScope = rememberCoroutineScope() val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + fun startDm(matrixUser: MatrixUser) { + startDmAction.value = Async.Uninitialized + val existingDM = matrixClient.findDM(matrixUser.id) + if (existingDM == null) { + localCoroutineScope.createDM(matrixUser, startDmAction) + } else { + startDmAction.value = Async.Success(existingDM.roomId) + } + } + fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.StartDM -> { - val existingDM = matrixClient.findDM(event.matrixUser.id) - if (existingDM == null) { - localCoroutineScope.createDM(event.matrixUser, startDmAction) - } else { - startDmAction.value = Async.Success(existingDM.roomId) - } + is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) + CreateRoomRootEvents.RetryStartDM -> { + startDmAction.value = Async.Uninitialized + selectUsersState.selectedUsers.firstOrNull()?.let { startDm(it) } } + CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index e0e3152318..e8f739c738 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -36,7 +36,18 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider { + ProgressDialog(text = stringResource(id = StringR.string.common_creating_room)) + } + is Async.Failure -> { + ErrorDialog( + content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), + dismissText = stringResource(id = StringR.string.action_cancel), + submitText = stringResource(id = StringR.string.action_retry), + onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, + onSubmit = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, + ) + } + else -> Unit } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index c3cacb25c7..6853fed3d8 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -18,18 +18,23 @@ package io.element.android.features.createroom.impl.root +import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter +import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.selectusers.api.aSelectUsersState import io.element.android.libraries.architecture.Async 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_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -38,15 +43,17 @@ import org.junit.Test class CreateRoomRootPresenterTests { private lateinit var presenter: CreateRoomRootPresenter + private lateinit var fakeSelectUsersPresenter: FakeSelectUserPresenter private lateinit var fakeMatrixClient: FakeMatrixClient @Before fun setup() { - val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { - override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) + val factory = object : SelectUsersPresenter.Factory { + override fun create(args: SelectUsersPresenterArgs) = fakeSelectUsersPresenter } + fakeSelectUsersPresenter = FakeSelectUserPresenter() fakeMatrixClient = FakeMatrixClient() - presenter = CreateRoomRootPresenter(selectUsersPresenter, fakeMatrixClient) + presenter = CreateRoomRootPresenter(factory, fakeMatrixClient) } @Test @@ -82,6 +89,7 @@ class CreateRoomRootPresenterTests { fakeMatrixClient.givenCreateDmResult(createDmResult) initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) val stateAfterStartDM = awaitItem() assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) @@ -105,4 +113,60 @@ class CreateRoomRootPresenterTests { assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId) } } + + @Test + fun `present - trigger retry create DM action`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val matrixUser = MatrixUser(UserId("@name:matrix.org")) + val createDmResult = Result.success(RoomId("!createDmResult")) + fakeSelectUsersPresenter.givenState(aSelectUsersState().copy(selectedUsers = persistentListOf(matrixUser))) + + fakeMatrixClient.givenFindDmResult(null) + fakeMatrixClient.givenCreateDmError(A_THROWABLE) + fakeMatrixClient.givenCreateDmResult(createDmResult) + + // Failure + initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) + val stateAfterStartDM = awaitItem() + assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Failure::class.java) + + // Cancel + stateAfterStartDM.eventSink(CreateRoomRootEvents.CancelStartDM) + val stateAfterCancel = awaitItem() + assertThat(stateAfterCancel.startDmAction).isInstanceOf(Async.Uninitialized::class.java) + + // Failure + stateAfterCancel.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) + val stateAfterSecondAttempt = awaitItem() + assertThat(stateAfterSecondAttempt.startDmAction).isInstanceOf(Async.Failure::class.java) + + // Retry with success + fakeMatrixClient.givenCreateDmError(null) + stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.RetryStartDM) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java) + assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) + val stateAfterRetryStartDM = awaitItem() + assertThat(stateAfterRetryStartDM.startDmAction).isInstanceOf(Async.Success::class.java) + assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) + } + } + + private class FakeSelectUserPresenter : SelectUsersPresenter { + + private var state = aSelectUsersState() + + fun givenState(state: SelectUsersState) { + this.state = state + } + + @Composable + override fun present(): SelectUsersState { + return state + } + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt index ce1730ed0c..da1aeb836e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt @@ -37,7 +37,9 @@ fun ErrorDialog( modifier: Modifier = Modifier, title: String = ErrorDialogDefaults.title, submitText: String = ErrorDialogDefaults.submitText, + dismissText: String? = null, onDismiss: () -> Unit = {}, + onSubmit: () -> Unit = onDismiss, shape: Shape = AlertDialogDefaults.shape, containerColor: Color = AlertDialogDefaults.containerColor, iconContentColor: Color = AlertDialogDefaults.iconContentColor, @@ -55,10 +57,17 @@ fun ErrorDialog( Text(content) }, confirmButton = { - TextButton(onClick = onDismiss) { + TextButton(onClick = onSubmit) { Text(submitText) } }, + dismissButton = dismissText?.let { + { + TextButton(onClick = onDismiss) { + Text(it) + } + } + }, shape = shape, containerColor = containerColor, iconContentColor = iconContentColor, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index ec5068e93d..5889d35e0c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -39,6 +39,7 @@ class FakeMatrixClient( ) : MatrixClient { private var createDmResult: Result = Result.success(A_ROOM_ID) + private var createDmFailure: Throwable? = null private var findDmResult: MatrixRoom? = FakeMatrixRoom() private var logoutFailure: Throwable? = null @@ -47,6 +48,8 @@ class FakeMatrixClient( } override suspend fun createDM(userId: UserId): Result { + delay(100) + createDmFailure?.let { throw it } return createDmResult } @@ -91,7 +94,7 @@ class FakeMatrixClient( // Mocks - fun givenLogoutError(failure: Throwable) { + fun givenLogoutError(failure: Throwable?) { logoutFailure = failure } @@ -99,6 +102,10 @@ class FakeMatrixClient( createDmResult = result } + fun givenCreateDmError(failure: Throwable?) { + createDmFailure = failure + } + fun givenFindDmResult(result: MatrixRoom?) { findDmResult = result } From f04d25c8af277cc9127011189c9a6ef36a09e218 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 29 Mar 2023 16:18:15 +0200 Subject: [PATCH 10/19] Add new screenshots --- ..._CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ..._CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1e92cf978d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f29da5d5aeb65659b065b7bd6afe276f83e020545a027780d2391308d1a4076 +size 20750 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..74acb423d6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7b91a2d05b975d568116615c286568f376ebead49e25ff17f5aae8b75be0e1f +size 28876 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..23c4de3194 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae3e8c4e952b97628d026dfe78781aef894d6c2e742ac6ae1f1a2c0170df159e +size 20382 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb7d1036e4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a35f28b3a59cc28937fee16eda26c26ef7b7622f929218e40f7537e096b2e8 +size 28120 From a335022499add281c85018c190e559de13b13358 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 30 Mar 2023 08:49:48 +0200 Subject: [PATCH 11/19] Pass null name when creating DM --- .../element/android/libraries/matrix/impl/RustMatrixClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index ce9286d2be..3dc630c42d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -167,7 +167,7 @@ class RustMatrixClient constructor( runCatching { val roomId = client.createRoom( CreateRoomParameters( - name = "", + name = null, topic = null, isEncrypted = true, isDirect = true, From 40b059147824e2508323527597089844e89825d4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 16:27:34 +0200 Subject: [PATCH 12/19] Move FakeSelectUserPresenter to dedicated module --- features/createroom/impl/build.gradle.kts | 1 + .../impl/addpeople/AddPeoplePresenterTests.kt | 8 ++--- .../impl/root/CreateRoomRootPresenterTests.kt | 17 +-------- features/selectusers/test/build.gradle.kts | 28 +++++++++++++++ .../test/FakeSelectUserPresenter.kt | 36 +++++++++++++++++++ .../test/FakeSelectUserPresenterFactory.kt | 25 +++++++++++++ 6 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 features/selectusers/test/build.gradle.kts create mode 100644 features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt create mode 100644 features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 366cc1e0bd..b68ad614e2 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.selectusers.impl) + testImplementation(projects.features.selectusers.test) androidTestImplementation(libs.test.junitext) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt index d9f40c1dbf..caf1544a8f 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt @@ -22,8 +22,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter +import io.element.android.features.selectusers.test.FakeSelectUserPresenterFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -35,10 +34,7 @@ class AddPeoplePresenterTests { @Before fun setup() { - val selectUsersFactory = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { - override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) - } - presenter = AddPeoplePresenter(selectUsersFactory) + presenter = AddPeoplePresenter(FakeSelectUserPresenterFactory()) } @Test diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 6853fed3d8..28f4668033 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -18,15 +18,14 @@ package io.element.android.features.createroom.impl.root -import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.selectusers.api.SelectUsersPresenter import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectUsersState import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.features.selectusers.test.FakeSelectUserPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -155,18 +154,4 @@ class CreateRoomRootPresenterTests { assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) } } - - private class FakeSelectUserPresenter : SelectUsersPresenter { - - private var state = aSelectUsersState() - - fun givenState(state: SelectUsersState) { - this.state = state - } - - @Composable - override fun present(): SelectUsersState { - return state - } - } } diff --git a/features/selectusers/test/build.gradle.kts b/features/selectusers/test/build.gradle.kts new file mode 100644 index 0000000000..fb4e505709 --- /dev/null +++ b/features/selectusers/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.selectusers.test" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.features.selectusers.api) +} diff --git a/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt new file mode 100644 index 0000000000..e92c99df92 --- /dev/null +++ b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.selectusers.test + +import androidx.compose.runtime.Composable +import io.element.android.features.selectusers.api.SelectUsersPresenter +import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.selectusers.api.aSelectUsersState + +class FakeSelectUserPresenter : SelectUsersPresenter { + + private var state = aSelectUsersState() + + fun givenState(state: SelectUsersState) { + this.state = state + } + + @Composable + override fun present(): SelectUsersState { + return state + } +} diff --git a/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt new file mode 100644 index 0000000000..7bf74370c9 --- /dev/null +++ b/features/selectusers/test/src/main/kotlin/io/element/android/features/selectusers/test/FakeSelectUserPresenterFactory.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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.selectusers.test + +import io.element.android.features.selectusers.api.SelectUsersPresenter +import io.element.android.features.selectusers.api.SelectUsersPresenterArgs + +class FakeSelectUserPresenterFactory : SelectUsersPresenter.Factory { + + override fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter = FakeSelectUserPresenter() +} From 2a1da1d405e0e692d32c6604cef038351b2b46b3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 16:41:09 +0200 Subject: [PATCH 13/19] Unplug DM creation --- .../createroom/impl/root/CreateRoomRootView.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 63f0216ec9..fb211febc2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -16,6 +16,7 @@ package io.element.android.features.createroom.impl.root +import android.widget.Toast import androidx.annotation.DrawableRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,6 +33,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -81,10 +83,16 @@ fun CreateRoomRootView( modifier = Modifier.padding(paddingValues), verticalArrangement = Arrangement.spacedBy(8.dp), ) { + val context = LocalContext.current SelectUsersView( modifier = Modifier.fillMaxWidth(), state = state.selectUsersState, - onUserSelected = { state.eventSink(CreateRoomRootEvents.StartDM(it)) }, + onUserSelected = { + // Fixme disabled DM creation since it can break the account data which is not correctly synced + // uncomment to enable it again or move behind a feature flag + Toast.makeText(context, "Create DM feature is disabled.", Toast.LENGTH_SHORT).show() +// state.eventSink(CreateRoomRootEvents.StartDM(it)) + }, ) if (!state.selectUsersState.isSearchActive) { From bedadd6c0de0a8dc5da0fc5591e4b75e46082965 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 16:21:10 +0200 Subject: [PATCH 14/19] Remove hardcoded string --- .../createroom/impl/root/CreateRoomRootStateProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index e8f739c738..85d9cb41c5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -38,7 +38,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider Date: Wed, 5 Apr 2023 12:02:10 +0200 Subject: [PATCH 15/19] Use RetryDialog --- .../impl/root/CreateRoomRootView.kt | 8 +- .../components/dialogs/ErrorDialog.kt | 11 +-- .../components/dialogs/RetryDialog.kt | 97 +++++++++++++++++++ 3 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index cc7fa853be..146c447996 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -44,7 +44,7 @@ import io.element.android.features.createroom.impl.R import io.element.android.features.userlist.api.UserListView import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar @@ -109,12 +109,10 @@ fun CreateRoomRootView( ProgressDialog(text = stringResource(id = StringR.string.common_creating_room)) } is Async.Failure -> { - ErrorDialog( + RetryDialog( content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), - dismissText = stringResource(id = StringR.string.action_cancel), - submitText = stringResource(id = StringR.string.action_retry), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, - onSubmit = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, + onRetry = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, ) } else -> Unit diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt index da1aeb836e..ce1730ed0c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt @@ -37,9 +37,7 @@ fun ErrorDialog( modifier: Modifier = Modifier, title: String = ErrorDialogDefaults.title, submitText: String = ErrorDialogDefaults.submitText, - dismissText: String? = null, onDismiss: () -> Unit = {}, - onSubmit: () -> Unit = onDismiss, shape: Shape = AlertDialogDefaults.shape, containerColor: Color = AlertDialogDefaults.containerColor, iconContentColor: Color = AlertDialogDefaults.iconContentColor, @@ -57,17 +55,10 @@ fun ErrorDialog( Text(content) }, confirmButton = { - TextButton(onClick = onSubmit) { + TextButton(onClick = onDismiss) { Text(submitText) } }, - dismissButton = dismissText?.let { - { - TextButton(onClick = onDismiss) { - Text(it) - } - } - }, shape = shape, containerColor = containerColor, iconContentColor = iconContentColor, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt new file mode 100644 index 0000000000..ebfa8effc8 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 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.libraries.designsystem.components.dialogs + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +fun RetryDialog( + content: String, + modifier: Modifier = Modifier, + title: String = RetryDialogDefaults.title, + retryText: String = RetryDialogDefaults.retryText, + dismissText: String = RetryDialogDefaults.dismissText, + onRetry: () -> Unit = {}, + onDismiss: () -> Unit = {}, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = AlertDialogDefaults.containerColor, + iconContentColor: Color = AlertDialogDefaults.iconContentColor, + titleContentColor: Color = AlertDialogDefaults.titleContentColor, + textContentColor: Color = AlertDialogDefaults.textContentColor, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, +) { + AlertDialog( + modifier = modifier, + onDismissRequest = onDismiss, + title = { + Text(title) + }, + text = { + Text(content) + }, + confirmButton = { + TextButton(onClick = onRetry) { + Text(retryText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(dismissText) + } + }, + shape = shape, + containerColor = containerColor, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + tonalElevation = tonalElevation, + ) +} + +object RetryDialogDefaults { + val title: String @Composable get() = stringResource(id = StringR.string.dialog_title_error) + val retryText: String @Composable get() = stringResource(id = StringR.string.action_retry) + val dismissText: String @Composable get() = stringResource(id = StringR.string.action_cancel) +} + +@Preview +@Composable +internal fun RetryDialogLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun RetryDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + RetryDialog( + content = "Content", + ) +} From 68de76640733e24bb32d51348d5be028c07c3d90 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 13:29:00 +0200 Subject: [PATCH 16/19] screenshots tests --- ...aultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..62dcdf219c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:596dee7cf5300dc5c2c6b314bf537619466f377b9946114e675c630e2e330976 +size 12059 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e1de911d46 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_RetryDialogLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f791b1c552138b0d3184b936a51982cfc3e23c0b7de2c9ef5c31559d9279245 +size 12113 From 86e260d29891fa74a92a6ebf71f341754bb5092d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 13:54:24 +0200 Subject: [PATCH 17/19] exclude fakes from code coverage --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d228aa2acb..0da25dc1bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -229,7 +229,7 @@ koverMerged { target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { includes += "*Presenter" - excludes += "*TemplatePresenter" + excludes += "*Fake*Presenter" } bound { minValue = 90 From 0da8b5a58dff027ece49d43da8f1322133bd6b53 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 17:33:52 +0200 Subject: [PATCH 18/19] Remove RetryStartDM action --- .../features/createroom/impl/root/CreateRoomRootEvents.kt | 1 - .../createroom/impl/root/CreateRoomRootPresenter.kt | 4 ---- .../features/createroom/impl/root/CreateRoomRootView.kt | 6 +++++- .../createroom/impl/root/CreateRoomRootPresenterTests.kt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index de9810f34a..87f35e4b1b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -21,6 +21,5 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface CreateRoomRootEvents { object InvitePeople : CreateRoomRootEvents data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents - object RetryStartDM : CreateRoomRootEvents object CancelStartDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 43ca377771..89932b0cdb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -69,10 +69,6 @@ class CreateRoomRootPresenter @Inject constructor( fun handleEvents(event: CreateRoomRootEvents) { when (event) { is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) - CreateRoomRootEvents.RetryStartDM -> { - startDmAction.value = Async.Uninitialized - userListState.selectedUsers.firstOrNull()?.let { startDm(it) } - } CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 146c447996..61b8b8738e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -112,7 +112,11 @@ fun CreateRoomRootView( RetryDialog( content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, - onRetry = { state.eventSink(CreateRoomRootEvents.RetryStartDM) }, + onRetry = { + state.userListState.selectedUsers.firstOrNull()?.let { + state.eventSink(CreateRoomRootEvents.StartDM(it)) + } + }, ) } else -> Unit diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index f1467485db..cf399fdbd3 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -145,7 +145,7 @@ class CreateRoomRootPresenterTests { // Retry with success fakeMatrixClient.givenCreateDmError(null) - stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.RetryStartDM) + stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) val stateAfterRetryStartDM = awaitItem() From 5bb31c1c3b25797fcfa6a44403a168fbb1e9e63f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 17:37:09 +0200 Subject: [PATCH 19/19] Cancel start DM if there is no more selected user --- .../features/createroom/impl/root/CreateRoomRootView.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 61b8b8738e..e488645b78 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -113,9 +113,10 @@ fun CreateRoomRootView( content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onRetry = { - state.userListState.selectedUsers.firstOrNull()?.let { - state.eventSink(CreateRoomRootEvents.StartDM(it)) - } + state.userListState.selectedUsers.firstOrNull() + ?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) } + // Cancel start DM if there is no more selected user (should not happen) + ?: state.eventSink(CreateRoomRootEvents.CancelStartDM) }, ) }