mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Let the user switch account from the room select screen when sharing
When several accounts are signed in, sharing into the app no longer goes through the account selection screen. The room select screen (Share mode) now displays the selected account and an account switch on top of the room list, with a design similar to the multi account section of the settings. Selecting another account displays the rooms of this account in place, scrolled to the top, without recreating the screen. The content is then shared from the room select screen using the selected account, with the same progress and error dialogs as when sharing with the current account. The current account is switched only once the share has succeeded, then the room is opened if the content has been shared to a single room. Implementation: - RoomSelectNode is now in the AppScope and uses MatrixClientProvider to get the room list of the selected session. - ShareNode gets the SharePresenter of the selected session from its session graph, using the new ShareBindings. - SessionGraphFactory is moved to the matrix api module so that it can be used by features. The account selection screen is still used for permalinks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -28,6 +28,7 @@ import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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.timeline.TimelineProvider
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
@@ -40,6 +41,7 @@ class ForwardMessagesNode(
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ForwardMessagesPresenter.Factory,
|
||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||
private val sessionId: SessionId,
|
||||
) : ParentNode<ForwardMessagesNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
@@ -62,7 +64,8 @@ class ForwardMessagesNode(
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
val callback = object : RoomSelectEntryPoint.Callback {
|
||||
override fun onRoomSelected(roomIds: List<RoomId>) {
|
||||
override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
// In Forward mode, the session cannot be changed
|
||||
presenter.onRoomSelected(roomIds)
|
||||
}
|
||||
|
||||
@@ -75,6 +78,7 @@ class ForwardMessagesNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = RoomSelectEntryPoint.Params(
|
||||
sessionId = sessionId,
|
||||
mode = RoomSelectMode.Forward,
|
||||
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||
),
|
||||
|
||||
+2
@@ -15,6 +15,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider
|
||||
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
@@ -39,6 +40,7 @@ class DefaultForwardEntryPointTest {
|
||||
plugins = plugins,
|
||||
presenterFactory = { _, _ -> createForwardMessagesPresenter() },
|
||||
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
||||
sessionId = A_SESSION_ID,
|
||||
)
|
||||
}
|
||||
val callback = object : ForwardEntryPoint.Callback {
|
||||
|
||||
+7
-1
@@ -13,6 +13,7 @@ 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
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface ShareEntryPoint : FeatureEntryPoint {
|
||||
data class Params(val shareIntentData: ShareIntentData)
|
||||
@@ -25,6 +26,11 @@ interface ShareEntryPoint : FeatureEntryPoint {
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onDone(roomIds: List<RoomId>)
|
||||
/**
|
||||
* Called when the share is done, or cancelled.
|
||||
* @param sessionId the session which has been used to share the data. It can be different from the current session.
|
||||
* @param roomIds the rooms the data has been shared to, empty if the share has been cancelled.
|
||||
*/
|
||||
fun onDone(sessionId: SessionId, roomIds: List<RoomId>)
|
||||
}
|
||||
}
|
||||
|
||||
+66
-9
@@ -11,7 +11,12 @@ package io.element.android.features.share.impl
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
@@ -23,14 +28,20 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.share.api.ShareEntryPoint
|
||||
import io.element.android.features.share.api.ShareIntentData
|
||||
import io.element.android.features.share.impl.di.ShareBindings
|
||||
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.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.SessionGraphFactory
|
||||
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.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
@@ -39,6 +50,9 @@ class ShareNode(
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: SharePresenter.Factory,
|
||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||
private val sessionId: SessionId,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val sessionGraphFactory: SessionGraphFactory,
|
||||
) : ParentNode<ShareNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
@@ -52,18 +66,57 @@ class ShareNode(
|
||||
|
||||
data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs
|
||||
|
||||
/**
|
||||
* The presenter used to share the data, and the session it belongs to.
|
||||
*/
|
||||
private data class ActiveShare(
|
||||
val sessionId: SessionId,
|
||||
val presenter: SharePresenter,
|
||||
)
|
||||
|
||||
private val inputs = inputs<Inputs>()
|
||||
private val presenter = presenterFactory.create(inputs.shareIntentData)
|
||||
private val callback: ShareEntryPoint.Callback = callback()
|
||||
private val currentSessionShare = ActiveShare(
|
||||
sessionId = sessionId,
|
||||
presenter = presenterFactory.create(inputs.shareIntentData),
|
||||
)
|
||||
private var activeShare by mutableStateOf(currentSessionShare)
|
||||
|
||||
private fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
if (sessionId == currentSessionShare.sessionId) {
|
||||
activeShare = currentSessionShare
|
||||
currentSessionShare.presenter.onRoomSelected(roomIds)
|
||||
} else if (sessionId == activeShare.sessionId) {
|
||||
// Retry with the same other session
|
||||
activeShare.presenter.onRoomSelected(roomIds)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
// Share using the graph of the selected session, the current session will be changed once the share is done
|
||||
matrixClientProvider.getOrRestore(sessionId)
|
||||
.onSuccess { client ->
|
||||
val presenterFactory = (sessionGraphFactory.create(client) as ShareBindings).sharePresenterFactory()
|
||||
activeShare = ActiveShare(
|
||||
sessionId = sessionId,
|
||||
presenter = presenterFactory.create(inputs.shareIntentData),
|
||||
).also {
|
||||
it.presenter.onRoomSelected(roomIds)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to get the client for session $sessionId")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
val callback = object : RoomSelectEntryPoint.Callback {
|
||||
override fun onRoomSelected(roomIds: List<RoomId>) {
|
||||
presenter.onRoomSelected(roomIds)
|
||||
override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||
this@ShareNode.onRoomSelected(sessionId, roomIds)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
callback.onDone(emptyList())
|
||||
callback.onDone(sessionId, emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +124,7 @@ class ShareNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
params = RoomSelectEntryPoint.Params(
|
||||
sessionId = sessionId,
|
||||
mode = RoomSelectMode.Share,
|
||||
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||
),
|
||||
@@ -86,11 +140,14 @@ class ShareNode(
|
||||
navModel = navModel,
|
||||
)
|
||||
|
||||
val state = presenter.present()
|
||||
ShareView(
|
||||
state = state,
|
||||
onShareSuccess = callback::onDone,
|
||||
)
|
||||
val currentShare = activeShare
|
||||
key(currentShare) {
|
||||
val state = currentShare.presenter.present()
|
||||
ShareView(
|
||||
state = state,
|
||||
onShareSuccess = { roomIds -> callback.onDone(currentShare.sessionId, roomIds) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.share.impl.di
|
||||
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import io.element.android.features.share.impl.SharePresenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
/**
|
||||
* Used to get a [SharePresenter.Factory] from the graph of a session which is not the current one.
|
||||
*/
|
||||
@ContributesTo(SessionScope::class)
|
||||
interface ShareBindings {
|
||||
fun sharePresenterFactory(): SharePresenter.Factory
|
||||
}
|
||||
+11
-1
@@ -14,8 +14,13 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.share.api.ShareEntryPoint
|
||||
import io.element.android.features.share.api.ShareIntentData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.SessionGraphFactory
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
@@ -39,10 +44,15 @@ class DefaultShareEntryPointTest {
|
||||
plugins = plugins,
|
||||
presenterFactory = { createSharePresenter() },
|
||||
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
||||
sessionId = A_SESSION_ID,
|
||||
matrixClientProvider = FakeMatrixClientProvider(),
|
||||
sessionGraphFactory = object : SessionGraphFactory {
|
||||
override fun create(client: MatrixClient): Any = lambdaError()
|
||||
},
|
||||
)
|
||||
}
|
||||
val callback = object : ShareEntryPoint.Callback {
|
||||
override fun onDone(roomIds: List<RoomId>) = lambdaError()
|
||||
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) = lambdaError()
|
||||
}
|
||||
val params = ShareEntryPoint.Params(
|
||||
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
|
||||
|
||||
Reference in New Issue
Block a user