AcceptDeclineInviteState: create ConfirmingDeclineInvite to host InviteData when confirming decline of invite.

This commit is contained in:
Benoit Marty 2024-10-14 11:19:18 +02:00
parent f2df8a2064
commit e490769444
8 changed files with 40 additions and 59 deletions

View file

@ -9,10 +9,8 @@ package io.element.android.features.invite.api.response
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import java.util.Optional
data class AcceptDeclineInviteState( data class AcceptDeclineInviteState(
val invite: Optional<InviteData>,
val acceptAction: AsyncAction<RoomId>, val acceptAction: AsyncAction<RoomId>,
val declineAction: AsyncAction<RoomId>, val declineAction: AsyncAction<RoomId>,
val eventSink: (AcceptDeclineInviteEvents) -> Unit, val eventSink: (AcceptDeclineInviteEvents) -> Unit,

View file

@ -10,23 +10,20 @@ package io.element.android.features.invite.api.response
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import java.util.Optional
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> { open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
override val values: Sequence<AcceptDeclineInviteState> override val values: Sequence<AcceptDeclineInviteState>
get() = sequenceOf( get() = sequenceOf(
anAcceptDeclineInviteState(), anAcceptDeclineInviteState(),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
invite = Optional.of( declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"), InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice")
), ),
declineAction = AsyncAction.ConfirmingNoParams,
), ),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
invite = Optional.of( declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room"), InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room")
), ),
declineAction = AsyncAction.ConfirmingNoParams,
), ),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
acceptAction = AsyncAction.Failure(Throwable("Whoops")), acceptAction = AsyncAction.Failure(Throwable("Whoops")),
@ -38,12 +35,10 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
} }
fun anAcceptDeclineInviteState( fun anAcceptDeclineInviteState(
invite: Optional<InviteData> = Optional.empty(),
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized, acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized, declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (AcceptDeclineInviteEvents) -> Unit = {} eventSink: (AcceptDeclineInviteEvents) -> Unit = {}
) = AcceptDeclineInviteState( ) = AcceptDeclineInviteState(
invite = invite,
acceptAction = acceptAction, acceptAction = acceptAction,
declineAction = declineAction, declineAction = declineAction,
eventSink = eventSink, eventSink = eventSink,

View file

@ -0,0 +1,14 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.invite.api.response
import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingDeclineInvite(
val inviteData: InviteData,
) : AsyncAction.Confirming

View file

@ -9,15 +9,13 @@ package io.element.android.features.invite.impl.response
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -29,9 +27,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class AcceptDeclineInvitePresenter @Inject constructor( class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
@ -43,35 +39,22 @@ class AcceptDeclineInvitePresenter @Inject constructor(
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
var currentInvite by remember {
mutableStateOf<Optional<InviteData>>(Optional.empty())
}
fun handleEvents(event: AcceptDeclineInviteEvents) { fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) { when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> { is AcceptDeclineInviteEvents.AcceptInvite -> {
// currentInvite is used to render the decline confirmation dialog
// and to reuse the roomId when the user confirm the rejection of the invitation.
// Just set it to empty here.
currentInvite = Optional.empty()
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
} }
is AcceptDeclineInviteEvents.DeclineInvite -> { is AcceptDeclineInviteEvents.DeclineInvite -> {
currentInvite = Optional.of(event.invite) declinedAction.value = ConfirmingDeclineInvite(event.invite)
declinedAction.value = AsyncAction.ConfirmingNoParams
} }
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized localCoroutineScope.declineInvite(event.roomId, declinedAction)
currentInvite.getOrNull()?.let {
localCoroutineScope.declineInvite(it.roomId, declinedAction)
}
currentInvite = Optional.empty()
} }
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
currentInvite = Optional.empty()
declinedAction.value = AsyncAction.Uninitialized declinedAction.value = AsyncAction.Uninitialized
} }
@ -86,7 +69,6 @@ class AcceptDeclineInvitePresenter @Inject constructor(
} }
return AcceptDeclineInviteState( return AcceptDeclineInviteState(
invite = currentInvite,
acceptAction = acceptedAction.value, acceptAction = acceptedAction.value,
declineAction = declinedAction.value, declineAction = declinedAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.impl.R import io.element.android.features.invite.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
@ -22,7 +23,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlin.jvm.optionals.getOrNull
@Composable @Composable
fun AcceptDeclineInviteView( fun AcceptDeclineInviteView(
@ -45,13 +45,13 @@ fun AcceptDeclineInviteView(
onErrorDismiss = { onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError) state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
}, },
confirmationDialog = { confirmationDialog = { confirming ->
val invite = state.invite.getOrNull() // Note: confirming will always be of type ConfirmingDeclineInvite.
if (invite != null) { if (confirming is ConfirmingDeclineInvite) {
DeclineConfirmationDialog( DeclineConfirmationDialog(
invite = invite, invite = confirming.inviteData,
onConfirmClick = { onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId))
}, },
onDismissClick = { onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)

View file

@ -8,9 +8,10 @@
package io.element.android.features.invite.impl.response package io.element.android.features.invite.impl.response
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View file

@ -10,6 +10,7 @@ package io.element.android.features.invite.impl.response
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -33,7 +34,6 @@ import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.Optional
class AcceptDeclineInvitePresenterTest { class AcceptDeclineInvitePresenterTest {
@get:Rule @get:Rule
@ -46,7 +46,6 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
} }
} }
} }
@ -61,17 +60,13 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData)) assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isInstanceOf(AsyncAction.Confirming::class.java)
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite InternalAcceptDeclineInviteEvents.CancelDeclineInvite
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
@ -93,22 +88,20 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
) )
} }
skipItems(2) assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.DismissDeclineError InternalAcceptDeclineInviteEvents.DismissDeclineError
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
@ -141,13 +134,13 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
) )
} }
skipItems(2) assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
} }
@ -173,7 +166,6 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
} }
awaitItem().also { state -> awaitItem().also { state ->
@ -183,7 +175,6 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
@ -220,7 +211,6 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
} }
awaitItem().also { state -> awaitItem().also { state ->

View file

@ -13,14 +13,15 @@ import io.element.android.libraries.matrix.api.room.InvitedRoom
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeInvitedRoom( class FakeInvitedRoom(
override val sessionId: SessionId = A_SESSION_ID, override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID, override val roomId: RoomId = A_ROOM_ID,
private val declineInviteResult: () -> Result<Unit> = { lambdaError() } private val declineInviteResult: () -> Result<Unit> = { lambdaError() }
) : InvitedRoom { ) : InvitedRoom {
override suspend fun declineInvite(): Result<Unit> { override suspend fun declineInvite(): Result<Unit> = simulateLongTask {
return declineInviteResult() declineInviteResult()
} }
override fun close() = Unit override fun close() = Unit