mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Keep the incoming share displayed when the current account is signed out
The incoming share was displayed in the logged in flow of the current session, so it was destroyed when the current session was signed out, for instance from another device. When another session was signed out, the room select screen was kept and displayed an error if this session was selected. To make the behavior consistent: - The incoming share is now displayed by the RootFlowNode, on top of the logged in flow, and does not depend on a session anymore. The latest used session is passed from the RootFlowNode to the room select screen, through the share flow, and is used as its initial session. - When the root of the RootFlowNode backstack changes while an incoming share is displayed, the new root is set below the incoming share, using the new replaceRoot backstack operation, instead of removing all the elements. - The room select screen handles the invalidation of the current session like for any other session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-3
@@ -10,12 +10,12 @@ package io.element.android.features.share.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.share.api.ShareEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultShareEntryPoint : ShareEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
@@ -26,7 +26,10 @@ class DefaultShareEntryPoint : ShareEntryPoint {
|
||||
return parentNode.createNode<ShareNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(
|
||||
ShareNode.Inputs(shareIntentData = params.shareIntentData),
|
||||
ShareNode.Inputs(
|
||||
sessionId = params.sessionId,
|
||||
shareIntentData = params.shareIntentData,
|
||||
),
|
||||
callback,
|
||||
)
|
||||
)
|
||||
|
||||
+10
-8
@@ -18,6 +18,7 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
@@ -26,21 +27,19 @@ import io.element.android.features.share.api.ShareIntentData
|
||||
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.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
class ShareNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: SharePresenter.Factory,
|
||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||
private val sessionId: SessionId,
|
||||
) : ParentNode<ShareNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
@@ -52,10 +51,13 @@ class ShareNode(
|
||||
@Parcelize
|
||||
object NavTarget : Parcelable
|
||||
|
||||
data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs
|
||||
data class Inputs(
|
||||
val sessionId: SessionId,
|
||||
val shareIntentData: ShareIntentData,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
private val presenter = presenterFactory.create(inputs.shareIntentData, sessionId)
|
||||
private val presenter = presenterFactory.create(inputs.shareIntentData)
|
||||
private val callback: ShareEntryPoint.Callback = callback()
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -65,7 +67,7 @@ class ShareNode(
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
callback.onDone(sessionId, emptyList())
|
||||
callback.onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ class ShareNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = RoomSelectEntryPoint.Params(
|
||||
sessionId = sessionId,
|
||||
sessionId = inputs.sessionId,
|
||||
mode = RoomSelectMode.Share,
|
||||
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||
),
|
||||
@@ -92,7 +94,7 @@ class ShareNode(
|
||||
val state = presenter.present()
|
||||
ShareView(
|
||||
state = state,
|
||||
onShareSuccess = { roomIds -> callback.onDone(state.sessionId, roomIds) },
|
||||
onShareSuccess = { result -> callback.onDone(result.sessionId, result.roomIds) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-11
@@ -10,9 +10,7 @@ package io.element.android.features.share.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
@@ -30,7 +28,6 @@ import kotlinx.coroutines.launch
|
||||
@AssistedInject
|
||||
class SharePresenter(
|
||||
@Assisted private val shareIntentData: ShareIntentData,
|
||||
@Assisted initialSessionId: SessionId,
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
@@ -38,24 +35,21 @@ class SharePresenter(
|
||||
) : Presenter<ShareState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
fun create(shareIntentData: ShareIntentData, initialSessionId: SessionId): SharePresenter
|
||||
fun create(shareIntentData: ShareIntentData): SharePresenter
|
||||
}
|
||||
|
||||
private val shareActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
|
||||
private val shareActionState: MutableState<AsyncAction<ShareResult>> = mutableStateOf(AsyncAction.Uninitialized)
|
||||
|
||||
/**
|
||||
* The session the data is shared with, it can be updated if the user selects rooms from another session.
|
||||
* Share the data to the rooms with [roomIds], which belong to the session [sessionId]. It can be another session than the current one.
|
||||
*/
|
||||
private var targetSessionId by mutableStateOf(initialSessionId)
|
||||
|
||||
fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
if (shareActionState.value.isLoading()) return
|
||||
targetSessionId = sessionId
|
||||
appCoroutineScope.launch {
|
||||
suspend {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
|
||||
shareDataSender.send(client, shareIntentData, roomIds).getOrThrow()
|
||||
roomIds
|
||||
ShareResult(sessionId = sessionId, roomIds = roomIds)
|
||||
}.runCatchingUpdatingState(shareActionState)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +63,6 @@ class SharePresenter(
|
||||
}
|
||||
|
||||
return ShareState(
|
||||
sessionId = targetSessionId,
|
||||
shareAction = shareActionState.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
+11
-2
@@ -13,7 +13,16 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
data class ShareState(
|
||||
val sessionId: SessionId,
|
||||
val shareAction: AsyncAction<List<RoomId>>,
|
||||
val shareAction: AsyncAction<ShareResult>,
|
||||
val eventSink: (ShareEvent) -> Unit
|
||||
)
|
||||
|
||||
/**
|
||||
* The result of a successful share.
|
||||
* @param sessionId the session which has been used to share the data, it can be another session than the current one.
|
||||
* @param roomIds the rooms the data has been shared to.
|
||||
*/
|
||||
data class ShareResult(
|
||||
val sessionId: SessionId,
|
||||
val roomIds: List<RoomId>,
|
||||
)
|
||||
|
||||
+11
-7
@@ -21,9 +21,7 @@ open class ShareStatePreviewParam : PreviewParameterProvider<ShareState> {
|
||||
shareAction = AsyncAction.Loading,
|
||||
),
|
||||
aShareState(
|
||||
shareAction = AsyncAction.Success(
|
||||
listOf(RoomId("!room2:domain")),
|
||||
)
|
||||
shareAction = AsyncAction.Success(aShareResult())
|
||||
),
|
||||
aShareState(
|
||||
shareAction = AsyncAction.Failure(RuntimeException("error")),
|
||||
@@ -31,12 +29,18 @@ open class ShareStatePreviewParam : PreviewParameterProvider<ShareState> {
|
||||
)
|
||||
}
|
||||
|
||||
fun aShareState(
|
||||
sessionId: SessionId = SessionId("@alice:server.org"),
|
||||
shareAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
|
||||
private fun aShareState(
|
||||
shareAction: AsyncAction<ShareResult> = AsyncAction.Uninitialized,
|
||||
eventSink: (ShareEvent) -> Unit = {}
|
||||
) = ShareState(
|
||||
sessionId = sessionId,
|
||||
shareAction = shareAction,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
private fun aShareResult(
|
||||
sessionId: SessionId = SessionId("@alice:server.org"),
|
||||
roomIds: List<RoomId> = listOf(RoomId("!room2:domain")),
|
||||
) = ShareResult(
|
||||
sessionId = sessionId,
|
||||
roomIds = roomIds,
|
||||
)
|
||||
|
||||
+1
-2
@@ -13,12 +13,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@Composable
|
||||
fun ShareView(
|
||||
state: ShareState,
|
||||
onShareSuccess: (List<RoomId>) -> Unit,
|
||||
onShareSuccess: (ShareResult) -> Unit,
|
||||
) {
|
||||
AsyncActionView(
|
||||
async = state.shareAction,
|
||||
|
||||
+4
-3
@@ -39,15 +39,16 @@ class DefaultShareEntryPointTest {
|
||||
ShareNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { _, _ -> createSharePresenter() },
|
||||
presenterFactory = { createSharePresenter() },
|
||||
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
||||
sessionId = A_SESSION_ID,
|
||||
)
|
||||
}
|
||||
val callback = object : ShareEntryPoint.Callback {
|
||||
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) = lambdaError()
|
||||
override fun onCancel() = lambdaError()
|
||||
}
|
||||
val params = ShareEntryPoint.Params(
|
||||
sessionId = A_SESSION_ID,
|
||||
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
|
||||
)
|
||||
val result = entryPoint.createNode(
|
||||
@@ -57,7 +58,7 @@ class DefaultShareEntryPointTest {
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(ShareNode::class.java)
|
||||
assertThat(result.plugins).contains(ShareNode.Inputs(params.shareIntentData))
|
||||
assertThat(result.plugins).contains(ShareNode.Inputs(params.sessionId, params.shareIntentData))
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-15
@@ -44,7 +44,6 @@ class SharePresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sessionId).isEqualTo(A_SESSION_ID)
|
||||
assertThat(initialState.shareAction.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -86,8 +85,7 @@ class SharePresenterTest {
|
||||
presenter.onRoomSelected(A_SESSION_ID, listOf(A_ROOM_ID))
|
||||
assertThat(awaitItem().shareAction.isLoading()).isTrue()
|
||||
val success = awaitItem()
|
||||
assertThat(success.sessionId).isEqualTo(A_SESSION_ID)
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(ShareResult(A_SESSION_ID, listOf(A_ROOM_ID))))
|
||||
sendResult.assertions().isCalledOnce().with(value(client), value(shareIntentData), value(listOf(A_ROOM_ID)))
|
||||
}
|
||||
}
|
||||
@@ -106,16 +104,11 @@ class SharePresenterTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sessionId).isEqualTo(A_SESSION_ID)
|
||||
skipItems(1)
|
||||
presenter.onRoomSelected(A_SESSION_ID_2, listOf(A_ROOM_ID))
|
||||
val sessionChanged = awaitItem()
|
||||
assertThat(sessionChanged.sessionId).isEqualTo(A_SESSION_ID_2)
|
||||
assertThat(sessionChanged.shareAction.isUninitialized()).isTrue()
|
||||
assertThat(awaitItem().shareAction.isLoading()).isTrue()
|
||||
val success = awaitItem()
|
||||
assertThat(success.sessionId).isEqualTo(A_SESSION_ID_2)
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(ShareResult(A_SESSION_ID_2, listOf(A_ROOM_ID))))
|
||||
getClient.assertions().isCalledOnce().with(value(A_SESSION_ID_2))
|
||||
sendResult.assertions().isCalledOnce()
|
||||
}
|
||||
@@ -133,7 +126,6 @@ class SharePresenterTest {
|
||||
}.test {
|
||||
skipItems(1)
|
||||
presenter.onRoomSelected(A_SESSION_ID_2, listOf(A_ROOM_ID))
|
||||
assertThat(awaitItem().sessionId).isEqualTo(A_SESSION_ID_2)
|
||||
assertThat(awaitItem().shareAction.isLoading()).isTrue()
|
||||
assertThat(awaitItem().shareAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||
sendResult.assertions().isNeverCalled()
|
||||
@@ -161,8 +153,8 @@ class SharePresenterTest {
|
||||
presenter.onRoomSelected(A_SESSION_ID_2, listOf(A_ROOM_ID))
|
||||
completable.complete(Result.success(Unit))
|
||||
val success = awaitItem()
|
||||
assertThat(success.sessionId).isEqualTo(A_SESSION_ID)
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
|
||||
// The second selection has been ignored
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(ShareResult(A_SESSION_ID, listOf(A_ROOM_ID))))
|
||||
sendResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
@@ -170,13 +162,11 @@ class SharePresenterTest {
|
||||
|
||||
internal fun TestScope.createSharePresenter(
|
||||
shareIntentData: ShareIntentData = ShareIntentData.PlainText(A_MESSAGE),
|
||||
initialSessionId: SessionId = A_SESSION_ID,
|
||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||
shareDataSender: ShareDataSender = FakeShareDataSender(),
|
||||
): SharePresenter {
|
||||
return SharePresenter(
|
||||
shareIntentData = shareIntentData,
|
||||
initialSessionId = initialSessionId,
|
||||
appCoroutineScope = this,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
shareDataSender = shareDataSender,
|
||||
|
||||
Reference in New Issue
Block a user