mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +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:
@@ -10,8 +10,8 @@ package io.element.android.x.di
|
|||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.appnav.di.SessionGraphFactory
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.SessionGraphFactory
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultSessionGraphFactory(
|
class DefaultSessionGraphFactory(
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ import dev.zacsweers.metro.AppScope
|
|||||||
import dev.zacsweers.metro.Assisted
|
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.appnav.di.SessionGraphFactory
|
|
||||||
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.callback
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.matrixmedia.api.ImageLoaderHolder
|
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ class LoggedInAppScopeFlowNode(
|
|||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToBugReport()
|
fun navigateToBugReport()
|
||||||
fun navigateToAddAccount()
|
fun navigateToAddAccount()
|
||||||
|
fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
@@ -91,6 +94,10 @@ class LoggedInAppScopeFlowNode(
|
|||||||
override fun navigateToAddAccount() {
|
override fun navigateToAddAccount() {
|
||||||
callback.navigateToAddAccount()
|
callback.navigateToAddAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?) {
|
||||||
|
callback.switchAccountAndOpenRoom(sessionId, roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
@@ -173,6 +174,7 @@ class LoggedInFlowNode(
|
|||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToBugReport()
|
fun navigateToBugReport()
|
||||||
fun navigateToAddAccount()
|
fun navigateToAddAccount()
|
||||||
|
fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
@@ -574,10 +576,16 @@ class LoggedInFlowNode(
|
|||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
|
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
|
||||||
callback = object : ShareEntryPoint.Callback {
|
callback = object : ShareEntryPoint.Callback {
|
||||||
override fun onDone(roomIds: List<RoomId>) {
|
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||||
// Remove the incoming share screen
|
// Remove the incoming share screen
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
|
|
||||||
|
if (sessionId != matrixClient.sessionId) {
|
||||||
|
// The data has been shared using another session, switch to it
|
||||||
|
callback.switchAccountAndOpenRoom(sessionId, roomIds.singleOrNull())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Navigate to the room if the text/media was shared to a single one
|
// Navigate to the room if the text/media was shared to a single one
|
||||||
roomIds.singleOrNull()?.let { roomId ->
|
roomIds.singleOrNull()?.let { roomId ->
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.accountprovider.AccountProvider
|
import io.element.android.libraries.matrix.api.accountprovider.AccountProvider
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.asEventId
|
import io.element.android.libraries.matrix.api.core.asEventId
|
||||||
@@ -349,8 +350,7 @@ class RootFlowNode(
|
|||||||
|
|
||||||
@Parcelize data class AccountSelect(
|
@Parcelize data class AccountSelect(
|
||||||
val currentSessionId: SessionId,
|
val currentSessionId: SessionId,
|
||||||
val shareIntentData: ShareIntentData?,
|
val permalinkData: PermalinkData,
|
||||||
val permalinkData: PermalinkData?,
|
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize data class NotLoggedInFlow(
|
@Parcelize data class NotLoggedInFlow(
|
||||||
@@ -389,6 +389,15 @@ class RootFlowNode(
|
|||||||
override fun navigateToAddAccount() {
|
override fun navigateToAddAccount() {
|
||||||
backstack.push(NavTarget.NotLoggedInFlow(null))
|
backstack.push(NavTarget.NotLoggedInFlow(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun switchAccountAndOpenRoom(sessionId: SessionId, roomId: RoomId?) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val loggedInFlowNode = attachSession(sessionId)
|
||||||
|
roomId?.let {
|
||||||
|
loggedInFlowNode.attachRoom(it.toRoomIdOrAlias(), clearBackstack = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
|
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
|
||||||
val buildContext = if (savedNavState != null) {
|
val buildContext = if (savedNavState != null) {
|
||||||
@@ -463,13 +472,7 @@ class RootFlowNode(
|
|||||||
// Do not pop when the account is changed to avoid a UI flicker.
|
// Do not pop when the account is changed to avoid a UI flicker.
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
attachSession(sessionId).apply {
|
attachSession(sessionId).attachPermalinkData(navTarget.permalinkData)
|
||||||
if (navTarget.shareIntentData != null) {
|
|
||||||
attachIncomingShare(navTarget.shareIntentData)
|
|
||||||
} else if (navTarget.permalinkData != null) {
|
|
||||||
attachPermalinkData(navTarget.permalinkData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,21 +542,9 @@ class RootFlowNode(
|
|||||||
// No session, open login
|
// No session, open login
|
||||||
switchToNotLoggedInFlow(null)
|
switchToNotLoggedInFlow(null)
|
||||||
} else {
|
} else {
|
||||||
// wait for the current session to be restored
|
// wait for the current session to be restored, then attach the incoming share node.
|
||||||
val loggedInFlowNode = attachSession(latestSessionId)
|
// In case of multiple accounts, the user can switch account from the room select screen.
|
||||||
if (sessionStore.numberOfSessions() > 1) {
|
attachSession(latestSessionId).attachIncomingShare(shareIntentData)
|
||||||
// Several accounts, let the user choose which one to use
|
|
||||||
backstack.push(
|
|
||||||
NavTarget.AccountSelect(
|
|
||||||
currentSessionId = latestSessionId,
|
|
||||||
shareIntentData = shareIntentData,
|
|
||||||
permalinkData = null,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Only one account, directly attach the incoming share node.
|
|
||||||
loggedInFlowNode.attachIncomingShare(shareIntentData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,7 +567,6 @@ class RootFlowNode(
|
|||||||
backstack.push(
|
backstack.push(
|
||||||
NavTarget.AccountSelect(
|
NavTarget.AccountSelect(
|
||||||
currentSessionId = latestSessionId,
|
currentSessionId = latestSessionId,
|
||||||
shareIntentData = null,
|
|
||||||
permalinkData = permalinkData,
|
permalinkData = permalinkData,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -20,13 +20,13 @@ import dev.zacsweers.metro.AppScope
|
|||||||
import dev.zacsweers.metro.Assisted
|
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.appnav.di.SessionGraphFactory
|
|
||||||
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
|
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
|
||||||
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.callback
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.api.SessionGraphFactory
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|||||||
+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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
@@ -40,6 +41,7 @@ class ForwardMessagesNode(
|
|||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: ForwardMessagesPresenter.Factory,
|
presenterFactory: ForwardMessagesPresenter.Factory,
|
||||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||||
|
private val sessionId: SessionId,
|
||||||
) : ParentNode<ForwardMessagesNode.NavTarget>(
|
) : ParentNode<ForwardMessagesNode.NavTarget>(
|
||||||
navModel = PermanentNavModel(
|
navModel = PermanentNavModel(
|
||||||
navTargets = setOf(NavTarget),
|
navTargets = setOf(NavTarget),
|
||||||
@@ -62,7 +64,8 @@ class ForwardMessagesNode(
|
|||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
val callback = object : RoomSelectEntryPoint.Callback {
|
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)
|
presenter.onRoomSelected(roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@ class ForwardMessagesNode(
|
|||||||
parentNode = this,
|
parentNode = this,
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
params = RoomSelectEntryPoint.Params(
|
params = RoomSelectEntryPoint.Params(
|
||||||
|
sessionId = sessionId,
|
||||||
mode = RoomSelectMode.Forward,
|
mode = RoomSelectMode.Forward,
|
||||||
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
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.features.forward.api.ForwardEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.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.matrix.test.timeline.FakeTimelineProvider
|
||||||
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
|
import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
@@ -39,6 +40,7 @@ class DefaultForwardEntryPointTest {
|
|||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
presenterFactory = { _, _ -> createForwardMessagesPresenter() },
|
presenterFactory = { _, _ -> createForwardMessagesPresenter() },
|
||||||
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : ForwardEntryPoint.Callback {
|
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 com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
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.SessionId
|
||||||
|
|
||||||
interface ShareEntryPoint : FeatureEntryPoint {
|
interface ShareEntryPoint : FeatureEntryPoint {
|
||||||
data class Params(val shareIntentData: ShareIntentData)
|
data class Params(val shareIntentData: ShareIntentData)
|
||||||
@@ -25,6 +26,11 @@ interface ShareEntryPoint : FeatureEntryPoint {
|
|||||||
): Node
|
): Node
|
||||||
|
|
||||||
interface Callback : Plugin {
|
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 android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
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.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
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.annotations.ContributesNode
|
||||||
import io.element.android.features.share.api.ShareEntryPoint
|
import io.element.android.features.share.api.ShareEntryPoint
|
||||||
import io.element.android.features.share.api.ShareIntentData
|
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.NodeInputs
|
||||||
import io.element.android.libraries.architecture.callback
|
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
|
||||||
|
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.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.RoomSelectEntryPoint
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
@@ -39,6 +50,9 @@ class ShareNode(
|
|||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: SharePresenter.Factory,
|
presenterFactory: SharePresenter.Factory,
|
||||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||||
|
private val sessionId: SessionId,
|
||||||
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
private val sessionGraphFactory: SessionGraphFactory,
|
||||||
) : ParentNode<ShareNode.NavTarget>(
|
) : ParentNode<ShareNode.NavTarget>(
|
||||||
navModel = PermanentNavModel(
|
navModel = PermanentNavModel(
|
||||||
navTargets = setOf(NavTarget),
|
navTargets = setOf(NavTarget),
|
||||||
@@ -52,18 +66,57 @@ class ShareNode(
|
|||||||
|
|
||||||
data class Inputs(val shareIntentData: ShareIntentData) : NodeInputs
|
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 inputs = inputs<Inputs>()
|
||||||
private val presenter = presenterFactory.create(inputs.shareIntentData)
|
|
||||||
private val callback: ShareEntryPoint.Callback = callback()
|
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 {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
val callback = object : RoomSelectEntryPoint.Callback {
|
val callback = object : RoomSelectEntryPoint.Callback {
|
||||||
override fun onRoomSelected(roomIds: List<RoomId>) {
|
override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) {
|
||||||
presenter.onRoomSelected(roomIds)
|
this@ShareNode.onRoomSelected(sessionId, roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel() {
|
override fun onCancel() {
|
||||||
callback.onDone(emptyList())
|
callback.onDone(sessionId, emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +124,7 @@ class ShareNode(
|
|||||||
parentNode = this,
|
parentNode = this,
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
params = RoomSelectEntryPoint.Params(
|
params = RoomSelectEntryPoint.Params(
|
||||||
|
sessionId = sessionId,
|
||||||
mode = RoomSelectMode.Share,
|
mode = RoomSelectMode.Share,
|
||||||
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||||
),
|
),
|
||||||
@@ -86,11 +140,14 @@ class ShareNode(
|
|||||||
navModel = navModel,
|
navModel = navModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = presenter.present()
|
val currentShare = activeShare
|
||||||
ShareView(
|
key(currentShare) {
|
||||||
state = state,
|
val state = currentShare.presenter.present()
|
||||||
onShareSuccess = callback::onDone,
|
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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.share.api.ShareEntryPoint
|
import io.element.android.features.share.api.ShareEntryPoint
|
||||||
import io.element.android.features.share.api.ShareIntentData
|
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.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_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.libraries.roomselect.test.FakeRoomSelectEntryPoint
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
@@ -39,10 +44,15 @@ class DefaultShareEntryPointTest {
|
|||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
presenterFactory = { createSharePresenter() },
|
presenterFactory = { createSharePresenter() },
|
||||||
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider(),
|
||||||
|
sessionGraphFactory = object : SessionGraphFactory {
|
||||||
|
override fun create(client: MatrixClient): Any = lambdaError()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : ShareEntryPoint.Callback {
|
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(
|
val params = ShareEntryPoint.Params(
|
||||||
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
|
shareIntentData = ShareIntentData.PlainText(A_MESSAGE),
|
||||||
|
|||||||
+5
-3
@@ -6,10 +6,12 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.libraries.matrix.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the dependency injection graph of a session.
|
||||||
|
* It can be used to get session scoped dependencies of a session which is not the current one.
|
||||||
|
*/
|
||||||
interface SessionGraphFactory {
|
interface SessionGraphFactory {
|
||||||
fun create(client: MatrixClient): Any
|
fun create(client: MatrixClient): Any
|
||||||
}
|
}
|
||||||
+12
-1
@@ -13,9 +13,16 @@ import com.bumble.appyx.core.node.Node
|
|||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
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.SessionId
|
||||||
|
|
||||||
interface RoomSelectEntryPoint : FeatureEntryPoint {
|
interface RoomSelectEntryPoint : FeatureEntryPoint {
|
||||||
|
/**
|
||||||
|
* @param sessionId the session to use initially. In [RoomSelectMode.Share] mode, the user can select another session.
|
||||||
|
* @param mode the mode of the screen.
|
||||||
|
* @param maxNumberOfRooms the maximum number of rooms the user can select.
|
||||||
|
*/
|
||||||
data class Params(
|
data class Params(
|
||||||
|
val sessionId: SessionId,
|
||||||
val mode: RoomSelectMode,
|
val mode: RoomSelectMode,
|
||||||
val maxNumberOfRooms: Int,
|
val maxNumberOfRooms: Int,
|
||||||
)
|
)
|
||||||
@@ -28,7 +35,11 @@ interface RoomSelectEntryPoint : FeatureEntryPoint {
|
|||||||
): Node
|
): Node
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRoomSelected(roomIds: List<RoomId>)
|
/**
|
||||||
|
* @param sessionId the session which has been selected by the user, the rooms belong to this session.
|
||||||
|
* @param roomIds the selected rooms.
|
||||||
|
*/
|
||||||
|
fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>)
|
||||||
fun onCancel()
|
fun onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ dependencies {
|
|||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.libraries.roomselect.api)
|
api(projects.libraries.roomselect.api)
|
||||||
|
|
||||||
testCommonDependencies(libs, includeTestComposeView = true)
|
testCommonDependencies(libs, includeTestComposeView = true)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -10,12 +10,12 @@ package io.element.android.libraries.roomselect.impl
|
|||||||
|
|
||||||
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 dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint {
|
class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint {
|
||||||
override fun createNode(
|
override fun createNode(
|
||||||
parentNode: Node,
|
parentNode: Node,
|
||||||
@@ -27,6 +27,7 @@ class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint {
|
|||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = listOf(
|
plugins = listOf(
|
||||||
RoomSelectNode.Inputs(
|
RoomSelectNode.Inputs(
|
||||||
|
sessionId = params.sessionId,
|
||||||
mode = params.mode,
|
mode = params.mode,
|
||||||
maxNumberOfRooms = params.maxNumberOfRooms,
|
maxNumberOfRooms = params.maxNumberOfRooms,
|
||||||
),
|
),
|
||||||
|
|||||||
+3
@@ -8,10 +8,13 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.roomselect.impl
|
package io.element.android.libraries.roomselect.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
|
|
||||||
sealed interface RoomSelectEvent {
|
sealed interface RoomSelectEvent {
|
||||||
data class ToggleSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvent
|
data class ToggleSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvent
|
||||||
data object ToggleSearchActive : RoomSelectEvent
|
data object ToggleSearchActive : RoomSelectEvent
|
||||||
data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvent
|
data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvent
|
||||||
|
data object ToggleAccountListExpanded : RoomSelectEvent
|
||||||
|
data class SelectAccount(val sessionId: SessionId) : RoomSelectEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||||||
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 com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
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
|
||||||
@@ -22,11 +23,11 @@ import io.element.android.libraries.architecture.NodeInputs
|
|||||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||||
import io.element.android.libraries.architecture.callback
|
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.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class RoomSelectNode(
|
class RoomSelectNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@@ -34,12 +35,14 @@ class RoomSelectNode(
|
|||||||
presenterFactory: RoomSelectPresenter.Factory,
|
presenterFactory: RoomSelectPresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
|
val sessionId: SessionId,
|
||||||
val mode: RoomSelectMode,
|
val mode: RoomSelectMode,
|
||||||
val maxNumberOfRooms: Int,
|
val maxNumberOfRooms: Int,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
private val presenter = presenterFactory.create(
|
private val presenter = presenterFactory.create(
|
||||||
|
initialSessionId = inputs.sessionId,
|
||||||
mode = inputs.mode,
|
mode = inputs.mode,
|
||||||
maxNumberOfRooms = inputs.maxNumberOfRooms,
|
maxNumberOfRooms = inputs.maxNumberOfRooms,
|
||||||
)
|
)
|
||||||
@@ -52,7 +55,7 @@ class RoomSelectNode(
|
|||||||
RoomSelectView(
|
RoomSelectView(
|
||||||
state = state,
|
state = state,
|
||||||
onDismiss = callback::onCancel,
|
onDismiss = callback::onCancel,
|
||||||
onSubmit = callback::onRoomSelected,
|
onSubmit = { roomIds -> callback.onRoomSelected(state.selectedAccount.userId, roomIds) },
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-10
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.roomselect.impl
|
package io.element.android.libraries.roomselect.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -15,7 +16,9 @@ import androidx.compose.runtime.State
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
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 androidx.compose.runtime.setValue
|
||||||
@@ -24,22 +27,34 @@ import dev.zacsweers.metro.AssistedFactory
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.api.user.toMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class RoomSelectPresenter(
|
class RoomSelectPresenter(
|
||||||
|
@Assisted private val initialSessionId: SessionId,
|
||||||
@Assisted private val mode: RoomSelectMode,
|
@Assisted private val mode: RoomSelectMode,
|
||||||
@Assisted private val maxNumberOfRooms: Int,
|
@Assisted private val maxNumberOfRooms: Int,
|
||||||
private val dataSourceFactory: RoomSelectSearchDataSource.Factory,
|
private val dataSourceFactory: RoomSelectSearchDataSource.Factory,
|
||||||
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
private val sessionStore: SessionStore,
|
||||||
) : Presenter<RoomSelectState> {
|
) : Presenter<RoomSelectState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
fun interface Factory {
|
fun interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
initialSessionId: SessionId,
|
||||||
mode: RoomSelectMode,
|
mode: RoomSelectMode,
|
||||||
maxNumberOfRooms: Int,
|
maxNumberOfRooms: Int,
|
||||||
): RoomSelectPresenter
|
): RoomSelectPresenter
|
||||||
@@ -50,18 +65,51 @@ class RoomSelectPresenter(
|
|||||||
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
|
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
|
||||||
val queryState = rememberTextFieldState()
|
val queryState = rememberTextFieldState()
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
|
var isAccountListExpanded by remember { mutableStateOf(false) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
var selectedSessionId by remember { mutableStateOf(initialSessionId) }
|
||||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
val accounts by remember {
|
||||||
|
sessionStore.sessionsFlow().map { list ->
|
||||||
val searchQuery = queryState.text.toString()
|
list.map { it.toMatrixUser() }
|
||||||
LaunchedEffect(searchQuery) {
|
}
|
||||||
dataSource.setSearchQuery(searchQuery)
|
}.collectAsState(initial = emptyList())
|
||||||
|
// Fallback to a MatrixUser with only the userId until the sessions are loaded
|
||||||
|
val selectedAccount = accounts.find { it.userId == selectedSessionId } ?: MatrixUser(selectedSessionId)
|
||||||
|
val otherAccounts by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
accounts.filter { it.userId != selectedSessionId }.toImmutableList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomSummaryDetailsList by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
// Create one data source per session. When the selected session changes, the previous
|
||||||
|
// data source is removed from the composition and its coroutine scope is cancelled.
|
||||||
|
val dataSource = key(selectedSessionId) {
|
||||||
|
val dataSourceCoroutineScope = rememberCoroutineScope()
|
||||||
|
// The sessions are restored at startup, so the client should already be in memory
|
||||||
|
val roomListService by produceState(matrixClientProvider.getOrNull(selectedSessionId)?.roomListService) {
|
||||||
|
if (value == null) {
|
||||||
|
value = matrixClientProvider.getOrRestore(selectedSessionId)
|
||||||
|
.onFailure { Timber.e(it, "Failed to get the client for $selectedSessionId") }
|
||||||
|
.getOrNull()
|
||||||
|
?.roomListService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remember(roomListService) {
|
||||||
|
roomListService?.let { dataSourceFactory.create(dataSourceCoroutineScope, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val searchResults by remember<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>> {
|
val searchQuery = queryState.text.toString()
|
||||||
|
LaunchedEffect(dataSource, searchQuery) {
|
||||||
|
dataSource?.setSearchQuery(searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a key so that the rooms of the previously selected session are not rendered while loading the new ones
|
||||||
|
val roomSummaryDetailsList by key(dataSource) {
|
||||||
|
(dataSource?.roomInfoList ?: flowOf(persistentListOf())).collectAsState(initial = persistentListOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchResults by remember<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>>(dataSource) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
when {
|
when {
|
||||||
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
|
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
|
||||||
@@ -82,8 +130,16 @@ class RoomSelectPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomSelectEvent.ToggleSearchActive -> isSearchActive = !isSearchActive
|
RoomSelectEvent.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||||
|
RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded
|
||||||
|
is RoomSelectEvent.SelectAccount -> {
|
||||||
|
selectedSessionId = event.sessionId
|
||||||
|
// The selected rooms and the search query are specific to the session
|
||||||
|
selectedRooms = persistentListOf()
|
||||||
|
queryState.clearText()
|
||||||
|
isAccountListExpanded = false
|
||||||
|
}
|
||||||
is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch {
|
is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
dataSource.updateVisibleRange(event.range)
|
dataSource?.updateVisibleRange(event.range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,6 +151,9 @@ class RoomSelectPresenter(
|
|||||||
searchQuery = queryState,
|
searchQuery = queryState,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
|
selectedAccount = selectedAccount,
|
||||||
|
otherAccounts = otherAccounts,
|
||||||
|
isAccountListExpanded = isAccountListExpanded,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -36,12 +36,12 @@ private const val PAGE_SIZE = 30
|
|||||||
@AssistedInject
|
@AssistedInject
|
||||||
class RoomSelectSearchDataSource(
|
class RoomSelectSearchDataSource(
|
||||||
@Assisted coroutineScope: CoroutineScope,
|
@Assisted coroutineScope: CoroutineScope,
|
||||||
roomListService: RoomListService,
|
@Assisted roomListService: RoomListService,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(coroutineScope: CoroutineScope): RoomSelectSearchDataSource
|
fun create(coroutineScope: CoroutineScope, roomListService: RoomListService): RoomSelectSearchDataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
private val roomList = roomListService.createRoomList(
|
private val roomList = roomListService.createRoomList(
|
||||||
|
|||||||
+9
@@ -10,6 +10,7 @@ package io.element.android.libraries.roomselect.impl
|
|||||||
|
|
||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -21,7 +22,15 @@ data class RoomSelectState(
|
|||||||
val searchQuery: TextFieldState,
|
val searchQuery: TextFieldState,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||||
|
val selectedAccount: MatrixUser,
|
||||||
|
val otherAccounts: ImmutableList<MatrixUser>,
|
||||||
|
val isAccountListExpanded: Boolean,
|
||||||
val eventSink: (RoomSelectEvent) -> Unit,
|
val eventSink: (RoomSelectEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val canSelectMoreRooms = selectedRooms.size < maxNumberOfRooms
|
val canSelectMoreRooms = selectedRooms.size < maxNumberOfRooms
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The account switch is only displayed when sharing, and if there are several accounts.
|
||||||
|
*/
|
||||||
|
val showAccountSwitch = mode == RoomSelectMode.Share && otherAccounts.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -13,6 +13,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
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.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
@@ -47,6 +49,19 @@ open class RoomSelectStatePreviewParam : PreviewParameterProvider<RoomSelectStat
|
|||||||
selectedRooms = aRoomSelectRoomList().subList(0, 1),
|
selectedRooms = aRoomSelectRoomList().subList(0, 1),
|
||||||
maxNumberOfRooms = 1,
|
maxNumberOfRooms = 1,
|
||||||
),
|
),
|
||||||
|
aRoomSelectState(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||||
|
selectedAccount = aMatrixUser(id = "@alice:example.org", displayName = "Alice"),
|
||||||
|
otherAccounts = anOtherAccountList(),
|
||||||
|
),
|
||||||
|
aRoomSelectState(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||||
|
selectedAccount = aMatrixUser(id = "@alice:example.org", displayName = "Alice"),
|
||||||
|
otherAccounts = anOtherAccountList(),
|
||||||
|
isAccountListExpanded = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +72,9 @@ internal fun aRoomSelectState(
|
|||||||
searchQuery: String = "",
|
searchQuery: String = "",
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
||||||
|
selectedAccount: MatrixUser = MatrixUser(UserId("@alice:example.org")),
|
||||||
|
otherAccounts: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
|
isAccountListExpanded: Boolean = false,
|
||||||
eventSink: (RoomSelectEvent) -> Unit = {},
|
eventSink: (RoomSelectEvent) -> Unit = {},
|
||||||
) = RoomSelectState(
|
) = RoomSelectState(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
@@ -65,9 +83,17 @@ internal fun aRoomSelectState(
|
|||||||
searchQuery = TextFieldState(initialText = searchQuery),
|
searchQuery = TextFieldState(initialText = searchQuery),
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
|
selectedAccount = selectedAccount,
|
||||||
|
otherAccounts = otherAccounts,
|
||||||
|
isAccountListExpanded = isAccountListExpanded,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun anOtherAccountList() = persistentListOf(
|
||||||
|
aMatrixUser(id = "@bob:example.org", displayName = "Bob"),
|
||||||
|
aMatrixUser(id = "@carol:server.org", displayName = "Carol"),
|
||||||
|
)
|
||||||
|
|
||||||
internal fun aRoomSelectRoomList() = persistentListOf(
|
internal fun aRoomSelectRoomList() = persistentListOf(
|
||||||
aSelectRoomInfo(
|
aSelectRoomInfo(
|
||||||
roomId = RoomId("!room1:domain"),
|
roomId = RoomId("!room1:domain"),
|
||||||
|
|||||||
+184
-58
@@ -9,6 +9,11 @@
|
|||||||
package io.element.android.libraries.roomselect.impl
|
package io.element.android.libraries.roomselect.impl
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -21,7 +26,6 @@ import androidx.compose.foundation.layout.asPaddingValues
|
|||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
@@ -31,25 +35,34 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.stateDescription
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarRow
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
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.designsystem.theme.components.Checkbox
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
@@ -60,6 +73,10 @@ import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffec
|
|||||||
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
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.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
@@ -68,6 +85,9 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=195-38510
|
||||||
|
*/
|
||||||
@Suppress("MultipleEmitters") // False positive
|
@Suppress("MultipleEmitters") // False positive
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomSelectView(
|
fun RoomSelectView(
|
||||||
@@ -111,7 +131,8 @@ fun RoomSelectView(
|
|||||||
onBack = { onBackButton(state) }
|
onBack = { onBackButton(state) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
// Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top
|
||||||
|
val lazyListState = key(state.selectedAccount.userId) { rememberLazyListState() }
|
||||||
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange))
|
state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange))
|
||||||
}
|
}
|
||||||
@@ -161,6 +182,12 @@ fun RoomSelectView(
|
|||||||
resultState = state.resultState,
|
resultState = state.resultState,
|
||||||
showBackButton = false,
|
showBackButton = false,
|
||||||
) { summaries ->
|
) { summaries ->
|
||||||
|
if (summaries.isNotEmpty()) {
|
||||||
|
ListSectionHeader(
|
||||||
|
title = stringResource(CommonStrings.common_header_rooms),
|
||||||
|
hasDivider = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding = lazyColumnContentPadding,
|
contentPadding = lazyColumnContentPadding,
|
||||||
@@ -172,7 +199,45 @@ fun RoomSelectView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
||||||
Column {
|
RoomSummaryView(
|
||||||
|
roomSummary,
|
||||||
|
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||||
|
onSelection = { roomSummary ->
|
||||||
|
state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary))
|
||||||
|
},
|
||||||
|
canBeSelected = state.canSelectMoreRooms,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isSearchActive) {
|
||||||
|
if (state.showAccountSwitch) {
|
||||||
|
AccountSwitchSection(
|
||||||
|
selectedAccount = state.selectedAccount,
|
||||||
|
otherAccounts = state.otherAccounts,
|
||||||
|
isExpanded = state.isAccountListExpanded,
|
||||||
|
onToggleExpand = { state.eventSink(RoomSelectEvent.ToggleAccountListExpanded) },
|
||||||
|
onSelectAccount = { state.eventSink(RoomSelectEvent.SelectAccount(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SelectedRoomsHelper(
|
||||||
|
selectedRooms = state.selectedRooms,
|
||||||
|
// showVerticalSpace only if there is no other accounts
|
||||||
|
showVerticalSpace = !state.showAccountSwitch,
|
||||||
|
)
|
||||||
|
if (state.resultState is SearchBarResultState.Results) {
|
||||||
|
if (state.resultState.results.isNotEmpty()) {
|
||||||
|
ListSectionHeader(
|
||||||
|
title = stringResource(CommonStrings.common_header_rooms),
|
||||||
|
hasDivider = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = lazyColumnContentPadding,
|
||||||
|
) {
|
||||||
|
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
||||||
RoomSummaryView(
|
RoomSummaryView(
|
||||||
roomSummary,
|
roomSummary,
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||||
@@ -181,34 +246,6 @@ fun RoomSelectView(
|
|||||||
},
|
},
|
||||||
canBeSelected = state.canSelectMoreRooms,
|
canBeSelected = state.canSelectMoreRooms,
|
||||||
)
|
)
|
||||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
|
||||||
SelectedRoomsHelper(
|
|
||||||
selectedRooms = state.selectedRooms,
|
|
||||||
showVerticalSpace = true,
|
|
||||||
)
|
|
||||||
if (state.resultState is SearchBarResultState.Results) {
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = lazyColumnContentPadding,
|
|
||||||
) {
|
|
||||||
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
|
||||||
Column {
|
|
||||||
RoomSummaryView(
|
|
||||||
roomSummary,
|
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
|
||||||
onSelection = { roomSummary ->
|
|
||||||
state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary))
|
|
||||||
},
|
|
||||||
canBeSelected = state.canSelectMoreRooms,
|
|
||||||
)
|
|
||||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +254,103 @@ fun RoomSelectView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to the MultiAccountSection of the PreferencesRootView.
|
||||||
|
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=5414-4759
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun AccountSwitchSection(
|
||||||
|
selectedAccount: MatrixUser,
|
||||||
|
otherAccounts: ImmutableList<MatrixUser>,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
onToggleExpand: () -> Unit,
|
||||||
|
onSelectAccount: (SessionId) -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
MatrixUserHeader(
|
||||||
|
matrixUser = selectedAccount,
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 8.dp,
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
)
|
||||||
|
val expandedStateDescription = if (isExpanded) {
|
||||||
|
stringResource(CommonStrings.a11y_state_expanded)
|
||||||
|
} else {
|
||||||
|
stringResource(CommonStrings.a11y_state_collapsed)
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
stateDescription = expandedStateDescription
|
||||||
|
},
|
||||||
|
content = { Text(stringResource(CommonStrings.common_switch_account)) },
|
||||||
|
onClick = onToggleExpand,
|
||||||
|
trailingContent = ListItemContent.Custom { _ ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !isExpanded,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
AvatarRow(
|
||||||
|
avatarDataList = otherAccounts
|
||||||
|
.take(3)
|
||||||
|
.map { it.getAvatarData(AvatarSize.OtherAccountItem) }
|
||||||
|
.toImmutableList(),
|
||||||
|
avatarType = AvatarType.User,
|
||||||
|
lastOnTop = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Animate the chevron icon to rotate when the section is expanded/collapsed
|
||||||
|
val rotation: Float by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) -180f else 0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
delayMillis = 0,
|
||||||
|
durationMillis = 300,
|
||||||
|
),
|
||||||
|
label = "chevron"
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.rotate(rotation),
|
||||||
|
imageVector = CompoundIcons.ChevronDown(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
)
|
||||||
|
otherAccounts.forEach { matrixUser ->
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
onSelectAccount(matrixUser.userId)
|
||||||
|
}
|
||||||
|
.padding(top = 2.dp, bottom = 2.dp, end = 8.dp),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
avatarSize = AvatarSize.AccountItem,
|
||||||
|
verticalSpaceWidth = 16.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 8.dp,
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectedRooms(
|
private fun SelectedRooms(
|
||||||
selectedRooms: ImmutableList<SelectRoomInfo>,
|
selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||||
@@ -241,28 +375,25 @@ private fun RoomSummaryView(
|
|||||||
canBeSelected: Boolean,
|
canBeSelected: Boolean,
|
||||||
onSelection: (SelectRoomInfo) -> Unit,
|
onSelection: (SelectRoomInfo) -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
ListItem(
|
||||||
modifier = Modifier
|
onClick = { onSelection(roomInfo) },
|
||||||
.clickable { onSelection(roomInfo) }
|
leadingContent = ListItemContent.Custom {
|
||||||
.fillMaxWidth()
|
Avatar(
|
||||||
.padding(start = 16.dp, end = 4.dp)
|
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
||||||
.heightIn(56.dp),
|
avatarType = AvatarType.Room(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
heroes = roomInfo.heroes.map { user ->
|
||||||
|
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
|
||||||
|
}.toImmutableList(),
|
||||||
|
isTombstoned = roomInfo.isTombstoned,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
enabled = isSelected || canBeSelected,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Column {
|
||||||
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
|
||||||
avatarType = AvatarType.Room(
|
|
||||||
heroes = roomInfo.heroes.map { user ->
|
|
||||||
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
|
|
||||||
}.toImmutableList(),
|
|
||||||
isTombstoned = roomInfo.isTombstoned,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
// Name
|
// Name
|
||||||
Text(
|
Text(
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
@@ -284,11 +415,6 @@ private fun RoomSummaryView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Checkbox(
|
|
||||||
checked = isSelected,
|
|
||||||
enabled = isSelected || canBeSelected,
|
|
||||||
onCheckedChange = { onSelection(roomInfo) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -12,6 +12,8 @@ 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.libraries.matrix.api.core.RoomId
|
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_SESSION_ID
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
@@ -36,7 +38,8 @@ class DefaultRoomSelectEntryPointTest : RobolectricTest() {
|
|||||||
RoomSelectNode(
|
RoomSelectNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
presenterFactory = { mode, maxNumberOfRooms ->
|
presenterFactory = { initialSessionId, mode, maxNumberOfRooms ->
|
||||||
|
assertThat(initialSessionId).isEqualTo(A_SESSION_ID)
|
||||||
assertThat(mode).isEqualTo(testMode)
|
assertThat(mode).isEqualTo(testMode)
|
||||||
assertThat(maxNumberOfRooms).isEqualTo(testMaxNumberOfRooms)
|
assertThat(maxNumberOfRooms).isEqualTo(testMaxNumberOfRooms)
|
||||||
createRoomSelectPresenter(mode, maxNumberOfRooms)
|
createRoomSelectPresenter(mode, maxNumberOfRooms)
|
||||||
@@ -44,10 +47,11 @@ class DefaultRoomSelectEntryPointTest : RobolectricTest() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : RoomSelectEntryPoint.Callback {
|
val callback = object : RoomSelectEntryPoint.Callback {
|
||||||
override fun onRoomSelected(roomIds: List<RoomId>) = lambdaError()
|
override fun onRoomSelected(sessionId: SessionId, roomIds: List<RoomId>) = lambdaError()
|
||||||
override fun onCancel() = lambdaError()
|
override fun onCancel() = lambdaError()
|
||||||
}
|
}
|
||||||
val params = RoomSelectEntryPoint.Params(
|
val params = RoomSelectEntryPoint.Params(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
mode = testMode,
|
mode = testMode,
|
||||||
maxNumberOfRooms = testMaxNumberOfRooms,
|
maxNumberOfRooms = testMaxNumberOfRooms,
|
||||||
)
|
)
|
||||||
@@ -58,7 +62,7 @@ class DefaultRoomSelectEntryPointTest : RobolectricTest() {
|
|||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
assertThat(result).isInstanceOf(RoomSelectNode::class.java)
|
assertThat(result).isInstanceOf(RoomSelectNode::class.java)
|
||||||
assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode, params.maxNumberOfRooms))
|
assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.sessionId, params.mode, params.maxNumberOfRooms))
|
||||||
assertThat(result.plugins).contains(callback)
|
assertThat(result.plugins).contains(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-2
@@ -11,15 +11,27 @@ package io.element.android.libraries.roomselect.impl
|
|||||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
@@ -46,9 +58,99 @@ class RoomSelectPresenterTest {
|
|||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.maxNumberOfRooms).isEqualTo(10)
|
assertThat(initialState.maxNumberOfRooms).isEqualTo(10)
|
||||||
assertThat(initialState.canSelectMoreRooms).isTrue()
|
assertThat(initialState.canSelectMoreRooms).isTrue()
|
||||||
|
assertThat(initialState.selectedAccount).isEqualTo(MatrixUser(A_SESSION_ID))
|
||||||
|
assertThat(initialState.otherAccounts).isEmpty()
|
||||||
|
assertThat(initialState.isAccountListExpanded).isFalse()
|
||||||
|
assertThat(initialState.showAccountSwitch).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - share mode with a single account does not show the account switch`() = runTest {
|
||||||
|
val presenter = createRoomSelectPresenter(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
sessionStore = InMemorySessionStore(
|
||||||
|
initialList = listOf(aSessionData(sessionId = A_SESSION_ID.value)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
assertThat(state.selectedAccount.userId).isEqualTo(A_SESSION_ID)
|
||||||
|
assertThat(state.otherAccounts).isEmpty()
|
||||||
|
assertThat(state.showAccountSwitch).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - share mode with several accounts shows the account switch`() = runTest {
|
||||||
|
val presenter = createRoomSelectPresenter(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
sessionStore = aSessionStoreWithTwoAccounts(),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
assertThat(state.selectedAccount.userId).isEqualTo(A_SESSION_ID)
|
||||||
|
assertThat(state.selectedAccount.displayName).isEqualTo("Alice")
|
||||||
|
assertThat(state.otherAccounts.map { it.userId }).containsExactly(A_SESSION_ID_2)
|
||||||
|
assertThat(state.showAccountSwitch).isTrue()
|
||||||
|
state.eventSink(RoomSelectEvent.ToggleAccountListExpanded)
|
||||||
|
assertThat(awaitItem().isAccountListExpanded).isTrue()
|
||||||
|
state.eventSink(RoomSelectEvent.ToggleAccountListExpanded)
|
||||||
|
assertThat(awaitItem().isAccountListExpanded).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - select another account displays the rooms of this account`() = runTest {
|
||||||
|
val roomSummary1 = aRoomSummary(roomId = A_ROOM_ID)
|
||||||
|
val roomSummary2 = aRoomSummary(roomId = A_ROOM_ID_2)
|
||||||
|
val roomListServices = mapOf(
|
||||||
|
A_SESSION_ID to FakeRoomListService(createRoomListLambda = { FakeDynamicRoomList(summaries = MutableStateFlow(listOf(roomSummary1))) }),
|
||||||
|
A_SESSION_ID_2 to FakeRoomListService(createRoomListLambda = { FakeDynamicRoomList(summaries = MutableStateFlow(listOf(roomSummary2))) }),
|
||||||
|
)
|
||||||
|
val presenter = createRoomSelectPresenter(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
sessionStore = aSessionStoreWithTwoAccounts(),
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider { sessionId ->
|
||||||
|
Result.success(FakeMatrixClient(sessionId = sessionId, roomListService = roomListServices.getValue(sessionId)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
assertThat(state.selectedAccount.userId).isEqualTo(A_SESSION_ID)
|
||||||
|
assertThat((state.resultState as SearchBarResultState.Results).results.map { it.roomId }).containsExactly(A_ROOM_ID)
|
||||||
|
state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary1.toSelectRoomInfo()))
|
||||||
|
state.eventSink(RoomSelectEvent.ToggleAccountListExpanded)
|
||||||
|
state.eventSink(RoomSelectEvent.SelectAccount(A_SESSION_ID_2))
|
||||||
|
val finalState = awaitLastSequentialItem()
|
||||||
|
assertThat(finalState.selectedAccount.userId).isEqualTo(A_SESSION_ID_2)
|
||||||
|
assertThat(finalState.otherAccounts.map { it.userId }).containsExactly(A_SESSION_ID)
|
||||||
|
assertThat(finalState.selectedRooms).isEmpty()
|
||||||
|
assertThat(finalState.isAccountListExpanded).isFalse()
|
||||||
|
assertThat((finalState.resultState as SearchBarResultState.Results).results.map { it.roomId }).containsExactly(A_ROOM_ID_2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - forward mode with several accounts does not show the account switch`() = runTest {
|
||||||
|
val presenter = createRoomSelectPresenter(
|
||||||
|
mode = RoomSelectMode.Forward,
|
||||||
|
sessionStore = aSessionStoreWithTwoAccounts(),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
assertThat(state.otherAccounts).isNotEmpty()
|
||||||
|
assertThat(state.showAccountSwitch).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aSessionStoreWithTwoAccounts() = InMemorySessionStore(
|
||||||
|
initialList = listOf(
|
||||||
|
aSessionData(sessionId = A_SESSION_ID.value, userDisplayName = "Alice"),
|
||||||
|
aSessionData(sessionId = A_SESSION_ID_2.value, userDisplayName = "Bob"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle search active`() = runTest {
|
fun `present - toggle search active`() = runTest {
|
||||||
val presenter = createRoomSelectPresenter()
|
val presenter = createRoomSelectPresenter()
|
||||||
@@ -157,16 +259,23 @@ internal fun TestScope.createRoomSelectPresenter(
|
|||||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||||
maxNumberOfRooms: Int = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
maxNumberOfRooms: Int = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS,
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider { sessionId ->
|
||||||
|
Result.success(FakeMatrixClient(sessionId = sessionId, roomListService = roomListService))
|
||||||
|
},
|
||||||
) = RoomSelectPresenter(
|
) = RoomSelectPresenter(
|
||||||
|
initialSessionId = A_SESSION_ID,
|
||||||
mode = mode,
|
mode = mode,
|
||||||
maxNumberOfRooms = maxNumberOfRooms,
|
maxNumberOfRooms = maxNumberOfRooms,
|
||||||
dataSourceFactory = object : RoomSelectSearchDataSource.Factory {
|
dataSourceFactory = object : RoomSelectSearchDataSource.Factory {
|
||||||
override fun create(coroutineScope: CoroutineScope): RoomSelectSearchDataSource {
|
override fun create(coroutineScope: CoroutineScope, roomListService: RoomListService): RoomSelectSearchDataSource {
|
||||||
return RoomSelectSearchDataSource(
|
return RoomSelectSearchDataSource(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
matrixClientProvider = matrixClientProvider,
|
||||||
|
sessionStore = sessionStore,
|
||||||
)
|
)
|
||||||
|
|||||||
+50
@@ -14,13 +14,19 @@ import androidx.compose.ui.test.AndroidComposeUiTest
|
|||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
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.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
|
import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
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.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -72,6 +78,50 @@ class RoomSelectViewTest : RobolectricTest() {
|
|||||||
onNodeWithText("Room with a single hero").assertIsDisplayed()
|
onNodeWithText("Room with a single hero").assertIsDisplayed()
|
||||||
onNodeWithText("@alice:example.org").assertDoesNotExist()
|
onNodeWithText("@alice:example.org").assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on switch account emits the expected event`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomSelectEvent>()
|
||||||
|
setRoomSelectView(
|
||||||
|
aRoomSelectState(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
selectedAccount = aMatrixUser(id = "@alice:example.org", displayName = "Alice"),
|
||||||
|
otherAccounts = persistentListOf(aMatrixUser(id = "@bob:example.org", displayName = "Bob")),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
clickOn(CommonStrings.common_switch_account)
|
||||||
|
// The first event is the initial UpdateVisibleRange
|
||||||
|
eventsRecorder.assertList(listOf(RoomSelectEvent.UpdateVisibleRange(0..-1), RoomSelectEvent.ToggleAccountListExpanded))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on another account emits the expected event`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomSelectEvent>()
|
||||||
|
setRoomSelectView(
|
||||||
|
aRoomSelectState(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
selectedAccount = aMatrixUser(id = "@alice:example.org", displayName = "Alice"),
|
||||||
|
otherAccounts = persistentListOf(aMatrixUser(id = "@bob:example.org", displayName = "Bob")),
|
||||||
|
isAccountListExpanded = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onNodeWithText("Bob").performClick()
|
||||||
|
// The first event is the initial UpdateVisibleRange
|
||||||
|
eventsRecorder.assertList(listOf(RoomSelectEvent.UpdateVisibleRange(0..-1), RoomSelectEvent.SelectAccount(UserId("@bob:example.org"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the account switch is not displayed if there is no other account`() = runAndroidComposeUiTest {
|
||||||
|
setRoomSelectView(
|
||||||
|
aRoomSelectState(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
selectedAccount = aMatrixUser(id = "@alice:example.org", displayName = "Alice"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onNodeWithText(activity!!.getString(CommonStrings.common_switch_account)).assertDoesNotExist()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AndroidComposeUiTest<ComponentActivity>.setRoomSelectView(
|
private fun AndroidComposeUiTest<ComponentActivity>.setRoomSelectView(
|
||||||
|
|||||||
Reference in New Issue
Block a user