Close the invite screen once the invite has been declined (#7440)

Declining an invite from the full screen invite left the user on that
same screen, so they had to press back themselves to get to the room
list. Both decline paths are affected: the plain decline handled by
AcceptDeclineInviteView, and the decline and block sub screen, which
navigated up into the join room flow instead of out of it.

Navigate up from the join room flow on a successful decline, and give
DeclineInviteAndBlockEntryPoint a callback so the sub screen can report
a successful decline separately from a back press. The room list flow
supplies the same callback, popping the sub screen once the invite has
been declined.
This commit is contained in:
hayyaksi
2026-08-19 13:52:11 +03:00
committed by GitHub
parent 03c69be722
commit 6bd841d838
10 changed files with 53 additions and 6 deletions
@@ -269,6 +269,11 @@ class HomeFlowNode(
parentNode = this, parentNode = this,
buildContext = buildContext, buildContext = buildContext,
inviteData = navTarget.inviteData, inviteData = navTarget.inviteData,
callback = object : DeclineInviteAndBlockEntryPoint.Callback {
override fun onDeclineSuccess() {
backstack.pop()
}
},
) )
} }
is NavTarget.SelectNewOwnersWhenLeavingRoom -> { is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
@@ -11,6 +11,7 @@ package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
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.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -36,7 +37,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() }, acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
directLogoutView = { _ -> lambdaError() }, directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() }, reportRoomEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() }, declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() }, changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
leaveRoomRenderer = { _, _, _ -> lambdaError() }, leaveRoomRenderer = { _, _, _ -> lambdaError() },
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
@@ -10,13 +10,19 @@ package io.element.android.features.invite.api.declineandblock
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint { interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
fun createNode( fun createNode(
parentNode: Node, parentNode: Node,
buildContext: BuildContext, buildContext: BuildContext,
inviteData: InviteData, inviteData: InviteData,
callback: Callback,
): Node ): Node
interface Callback : Plugin {
fun onDeclineSuccess()
}
} }
@@ -17,7 +17,9 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@@ -31,6 +33,7 @@ class DeclineAndBlockNode(
data class Inputs(val inviteData: InviteData) : NodeInputs data class Inputs(val inviteData: InviteData) : NodeInputs
private val inviteData = inputs<Inputs>().inviteData private val inviteData = inputs<Inputs>().inviteData
private val callback: DeclineInviteAndBlockEntryPoint.Callback = callback()
private val presenter = presenterFactory.create(inviteData) private val presenter = presenterFactory.create(inviteData)
@Composable @Composable
@@ -39,6 +42,7 @@ class DeclineAndBlockNode(
DeclineAndBlockView( DeclineAndBlockView(
state = state, state = state,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onDeclineSuccess = callback::onDeclineSuccess,
modifier = modifier modifier = modifier
) )
} }
@@ -46,6 +46,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun DeclineAndBlockView( fun DeclineAndBlockView(
state: DeclineAndBlockState, state: DeclineAndBlockState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onDeclineSuccess: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@@ -53,7 +54,7 @@ fun DeclineAndBlockView(
val isDeclining = state.declineAction is AsyncAction.Loading val isDeclining = state.declineAction is AsyncAction.Loading
AsyncActionView( AsyncActionView(
async = state.declineAction, async = state.declineAction,
onSuccess = { onBackClick() }, onSuccess = { onDeclineSuccess() },
errorMessage = { stringResource(CommonStrings.error_unknown) }, errorMessage = { stringResource(CommonStrings.error_unknown) },
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) }, onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) } onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
@@ -149,5 +150,6 @@ internal fun DeclineAndBlockViewPreview(
DeclineAndBlockView( DeclineAndBlockView(
state = state, state = state,
onBackClick = {}, onBackClick = {},
onDeclineSuccess = {},
) )
} }
@@ -22,8 +22,9 @@ class DefaultDeclineAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
parentNode: Node, parentNode: Node,
buildContext: BuildContext, buildContext: BuildContext,
inviteData: InviteData, inviteData: InviteData,
callback: DeclineInviteAndBlockEntryPoint.Callback,
): Node { ): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData) val inputs = DeclineAndBlockNode.Inputs(inviteData)
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs)) return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs, callback))
} }
} }
@@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.invite.impl.R import io.element.android.features.invite.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@@ -41,6 +42,20 @@ class DeclineAndBlockViewTest : RobolectricTest() {
} }
} }
@Test
fun `a successful decline invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
declineAction = AsyncAction.Success(Unit),
eventSink = eventsRecorder,
),
onDeclineSuccess = it,
)
}
}
@Test @Test
fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest { fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>() val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
@@ -112,11 +127,13 @@ class DeclineAndBlockViewTest : RobolectricTest() {
private fun AndroidComposeUiTest<ComponentActivity>.setDeclineAndBlockView( private fun AndroidComposeUiTest<ComponentActivity>.setDeclineAndBlockView(
state: DeclineAndBlockState, state: DeclineAndBlockState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onDeclineSuccess: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
DeclineAndBlockView( DeclineAndBlockView(
state = state, state = state,
onBackClick = onBackClick, onBackClick = onBackClick,
onDeclineSuccess = onDeclineSuccess,
) )
} }
} }
@@ -11,7 +11,9 @@ package io.element.android.features.invite.impl.declineandblock
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.invite.test.anInviteData import io.element.android.features.invite.test.anInviteData
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -31,12 +33,17 @@ class DefaultDeclineAndBlockEntryPointTest {
) )
} }
val inviteData = anInviteData() val inviteData = anInviteData()
val callback = object : DeclineInviteAndBlockEntryPoint.Callback {
override fun onDeclineSuccess() = lambdaError()
}
val result = entryPoint.createNode( val result = entryPoint.createNode(
parentNode = parentNode, parentNode = parentNode,
buildContext = BuildContext.root(null), buildContext = BuildContext.root(null),
inviteData = inviteData inviteData = inviteData,
callback = callback,
) )
assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java) assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java)
assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData)) assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData))
assertThat(result.plugins).contains(callback)
} }
} }
@@ -19,6 +19,7 @@ class FakeDeclineInviteAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
parentNode: Node, parentNode: Node,
buildContext: BuildContext, buildContext: BuildContext,
inviteData: InviteData, inviteData: InviteData,
callback: DeclineInviteAndBlockEntryPoint.Callback,
): Node { ): Node {
lambdaError() lambdaError()
} }
@@ -69,6 +69,9 @@ class JoinRoomFlowNode(
parentNode = this, parentNode = this,
buildContext = buildContext, buildContext = buildContext,
inviteData = navTarget.inviteData, inviteData = navTarget.inviteData,
callback = object : DeclineInviteAndBlockEntryPoint.Callback {
override fun onDeclineSuccess() = navigateUp()
},
) )
NavTarget.Root -> rootNode(buildContext) NavTarget.Root -> rootNode(buildContext)
} }
@@ -99,7 +102,7 @@ class JoinRoomFlowNode(
acceptDeclineInviteView.Render( acceptDeclineInviteView.Render(
state = state.acceptDeclineInviteState, state = state.acceptDeclineInviteState,
onAcceptInviteSuccess = {}, onAcceptInviteSuccess = {},
onDeclineInviteSuccess = {}, onDeclineInviteSuccess = { navigateUp() },
modifier = Modifier modifier = Modifier
) )
} }