Handle errors on create DM
This commit is contained in:
parent
e325c58cb0
commit
f65375b5c4
7 changed files with 130 additions and 16 deletions
|
|
@ -21,4 +21,6 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
sealed interface CreateRoomRootEvents {
|
sealed interface CreateRoomRootEvents {
|
||||||
object InvitePeople : CreateRoomRootEvents
|
object InvitePeople : CreateRoomRootEvents
|
||||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||||
|
object RetryStartDM : CreateRoomRootEvents
|
||||||
|
object CancelStartDM : CreateRoomRootEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,16 +50,24 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val startDmAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
val startDmAction: MutableState<Async<RoomId>> = 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) {
|
fun handleEvents(event: CreateRoomRootEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is CreateRoomRootEvents.StartDM -> {
|
is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser)
|
||||||
val existingDM = matrixClient.findDM(event.matrixUser.id)
|
CreateRoomRootEvents.RetryStartDM -> {
|
||||||
if (existingDM == null) {
|
startDmAction.value = Async.Uninitialized
|
||||||
localCoroutineScope.createDM(event.matrixUser, startDmAction)
|
selectUsersState.selectedUsers.firstOrNull()?.let { startDm(it) }
|
||||||
} else {
|
|
||||||
startDmAction.value = Async.Success(existingDM.roomId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized
|
||||||
CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action
|
CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,18 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
aCreateRoomRootState().copy(
|
||||||
|
startDmAction = Async.Failure(Throwable("An error occurred when trying to start a chat")),
|
||||||
|
selectUsersState = aMatrixUser().let {
|
||||||
|
aSelectUsersState().copy(
|
||||||
|
searchQuery = it.id.value,
|
||||||
|
searchResults = persistentListOf(it),
|
||||||
|
selectedUsers = persistentListOf(it),
|
||||||
|
isSearchActive = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.selectusers.api.SelectUsersView
|
import io.element.android.features.selectusers.api.SelectUsersView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||||
|
|
@ -95,8 +96,20 @@ fun CreateRoomRootView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.startDmAction is Async.Loading) {
|
when (state.startDmAction) {
|
||||||
ProgressDialog(text = stringResource(id = StringR.string.common_creating_room))
|
is Async.Loading -> {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,23 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.selectusers.api.SelectUsersPresenter
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
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.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
@ -38,15 +43,17 @@ import org.junit.Test
|
||||||
class CreateRoomRootPresenterTests {
|
class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
private lateinit var presenter: CreateRoomRootPresenter
|
private lateinit var presenter: CreateRoomRootPresenter
|
||||||
|
private lateinit var fakeSelectUsersPresenter: FakeSelectUserPresenter
|
||||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
val factory = object : SelectUsersPresenter.Factory {
|
||||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
override fun create(args: SelectUsersPresenterArgs) = fakeSelectUsersPresenter
|
||||||
}
|
}
|
||||||
|
fakeSelectUsersPresenter = FakeSelectUserPresenter()
|
||||||
fakeMatrixClient = FakeMatrixClient()
|
fakeMatrixClient = FakeMatrixClient()
|
||||||
presenter = CreateRoomRootPresenter(selectUsersPresenter, fakeMatrixClient)
|
presenter = CreateRoomRootPresenter(factory, fakeMatrixClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -82,6 +89,7 @@ class CreateRoomRootPresenterTests {
|
||||||
fakeMatrixClient.givenCreateDmResult(createDmResult)
|
fakeMatrixClient.givenCreateDmResult(createDmResult)
|
||||||
|
|
||||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||||
|
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||||
val stateAfterStartDM = awaitItem()
|
val stateAfterStartDM = awaitItem()
|
||||||
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java)
|
||||||
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull())
|
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull())
|
||||||
|
|
@ -105,4 +113,60 @@ class CreateRoomRootPresenterTests {
|
||||||
assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ fun ErrorDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String = ErrorDialogDefaults.title,
|
title: String = ErrorDialogDefaults.title,
|
||||||
submitText: String = ErrorDialogDefaults.submitText,
|
submitText: String = ErrorDialogDefaults.submitText,
|
||||||
|
dismissText: String? = null,
|
||||||
onDismiss: () -> Unit = {},
|
onDismiss: () -> Unit = {},
|
||||||
|
onSubmit: () -> Unit = onDismiss,
|
||||||
shape: Shape = AlertDialogDefaults.shape,
|
shape: Shape = AlertDialogDefaults.shape,
|
||||||
containerColor: Color = AlertDialogDefaults.containerColor,
|
containerColor: Color = AlertDialogDefaults.containerColor,
|
||||||
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||||
|
|
@ -55,10 +57,17 @@ fun ErrorDialog(
|
||||||
Text(content)
|
Text(content)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onSubmit) {
|
||||||
Text(submitText)
|
Text(submitText)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dismissButton = dismissText?.let {
|
||||||
|
{
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = shape,
|
shape = shape,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
iconContentColor = iconContentColor,
|
iconContentColor = iconContentColor,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class FakeMatrixClient(
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
|
|
||||||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||||
|
private var createDmFailure: Throwable? = null
|
||||||
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
|
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
|
||||||
private var logoutFailure: Throwable? = null
|
private var logoutFailure: Throwable? = null
|
||||||
|
|
||||||
|
|
@ -47,6 +48,8 @@ class FakeMatrixClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createDM(userId: UserId): Result<RoomId> {
|
override suspend fun createDM(userId: UserId): Result<RoomId> {
|
||||||
|
delay(100)
|
||||||
|
createDmFailure?.let { throw it }
|
||||||
return createDmResult
|
return createDmResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +94,7 @@ class FakeMatrixClient(
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
|
|
||||||
fun givenLogoutError(failure: Throwable) {
|
fun givenLogoutError(failure: Throwable?) {
|
||||||
logoutFailure = failure
|
logoutFailure = failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +102,10 @@ class FakeMatrixClient(
|
||||||
createDmResult = result
|
createDmResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCreateDmError(failure: Throwable?) {
|
||||||
|
createDmFailure = failure
|
||||||
|
}
|
||||||
|
|
||||||
fun givenFindDmResult(result: MatrixRoom?) {
|
fun givenFindDmResult(result: MatrixRoom?) {
|
||||||
findDmResult = result
|
findDmResult = result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue