Merge branch 'develop' into feature/fga/space_list_join_action
This commit is contained in:
commit
6eae9e379f
591 changed files with 6155 additions and 2140 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.2.10" />
|
||||
<option name="version" value="2.2.20" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -8,6 +8,6 @@ appId: ${MAESTRO_APP_ID}
|
|||
- hideKeyboard
|
||||
- tapOn: "Continue"
|
||||
- extendedWaitUntil:
|
||||
visible: "Verification complete"
|
||||
visible: "Device verified"
|
||||
timeout: 30000
|
||||
- tapOn: "Continue"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import io.element.android.x.BuildConfig
|
|||
import io.element.android.x.R
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.plus
|
||||
import java.io.File
|
||||
|
|
@ -107,11 +106,7 @@ object AppModule {
|
|||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||
return CoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
)
|
||||
return CoroutineDispatchers.Default
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ dependencies {
|
|||
allFeaturesApi(project)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.accountselect.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.deeplink.api)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.SessionGraphFactory
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -41,7 +41,7 @@ import kotlinx.parcelize.Parcelize
|
|||
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
|
||||
*/
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoggedInAppScopeFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
@ -57,6 +57,7 @@ class LoggedInAppScopeFlowNode(
|
|||
), DependencyInjectionGraphOwner {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
fun onAddAccount()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
|
@ -83,6 +84,10 @@ class LoggedInAppScopeFlowNode(
|
|||
override fun onOpenBugReport() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
}
|
||||
|
||||
override fun onAddAccount() {
|
||||
plugins<Callback>().forEach { it.onAddAccount() }
|
||||
}
|
||||
}
|
||||
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,17 @@ class LoggedInEventProcessor(
|
|||
observingJob = roomMembershipObserver.updates
|
||||
.filter { !it.isUserInRoom }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
when (it.change) {
|
||||
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
|
||||
.onEach { roomMemberShipUpdate ->
|
||||
when (roomMemberShipUpdate.change) {
|
||||
MembershipChange.LEFT -> {
|
||||
displayMessage(
|
||||
if (roomMemberShipUpdate.isSpace) {
|
||||
CommonStrings.common_current_user_left_space
|
||||
} else {
|
||||
CommonStrings.common_current_user_left_room
|
||||
}
|
||||
)
|
||||
}
|
||||
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
|
||||
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
|
||||
else -> Unit
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import com.bumble.appyx.navmodel.backstack.operation.replace
|
|||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.Inject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.annotations.ContributesNode
|
||||
|
|
@ -75,7 +76,6 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
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.SessionId
|
||||
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.permalink.PermalinkData
|
||||
|
|
@ -100,7 +100,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
import kotlin.time.toKotlinDuration
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoggedInFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
@ -138,6 +138,7 @@ class LoggedInFlowNode(
|
|||
) {
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
fun onAddAccount()
|
||||
}
|
||||
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
|
|
@ -392,6 +393,10 @@ class LoggedInFlowNode(
|
|||
}
|
||||
is NavTarget.Settings -> {
|
||||
val callback = object : PreferencesEntryPoint.Callback {
|
||||
override fun onAddAccount() {
|
||||
plugins<Callback>().forEach { it.onAddAccount() }
|
||||
}
|
||||
|
||||
override fun onOpenBugReport() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
}
|
||||
|
|
@ -404,11 +409,7 @@ class LoggedInFlowNode(
|
|||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||
}
|
||||
|
||||
override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||
// We do not check the sessionId, but it will have to be done at some point (multi account)
|
||||
if (sessionId != matrixClient.sessionId) {
|
||||
Timber.e("SessionId mismatch, expected ${matrixClient.sessionId} but got $sessionId")
|
||||
}
|
||||
override fun navigateTo(roomId: RoomId, eventId: EventId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.api.LoginParams
|
||||
|
|
@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactor
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class NotLoggedInFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ package io.element.android.appnav
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -23,9 +25,11 @@ import com.bumble.appyx.core.state.MutableSavedStateMap
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
|
||||
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.MatrixSessionCache
|
||||
|
|
@ -39,13 +43,17 @@ import io.element.android.features.login.api.accesscontrol.AccountProviderAccess
|
|||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
|
|
@ -56,12 +64,11 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
class RootFlowNode(
|
||||
@ContributesNode(AppScope::class) @AssistedInject class RootFlowNode(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val sessionStore: SessionStore,
|
||||
|
|
@ -71,9 +78,11 @@ class RootFlowNode(
|
|||
private val presenter: RootPresenter,
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val accountSelectEntryPoint: AccountSelectEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val bugReporter: BugReporter,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : BaseFlowNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
|
|
@ -95,27 +104,24 @@ class RootFlowNode(
|
|||
}
|
||||
|
||||
private fun observeNavState() {
|
||||
navStateFlowFactory.create(buildContext.savedStateMap)
|
||||
.distinctUntilChanged()
|
||||
.onEach { navState ->
|
||||
Timber.v("navState=$navState")
|
||||
when (navState.loggedInState) {
|
||||
is LoggedInState.LoggedIn -> {
|
||||
if (navState.loggedInState.isTokenValid) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow(null) }
|
||||
)
|
||||
} else {
|
||||
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||
}
|
||||
}
|
||||
LoggedInState.NotLoggedIn -> {
|
||||
switchToNotLoggedInFlow(null)
|
||||
navStateFlowFactory.create(buildContext.savedStateMap).distinctUntilChanged().onEach { navState ->
|
||||
Timber.v("navState=$navState")
|
||||
when (navState.loggedInState) {
|
||||
is LoggedInState.LoggedIn -> {
|
||||
if (navState.loggedInState.isTokenValid) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow(null) }
|
||||
)
|
||||
} else {
|
||||
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||
}
|
||||
}
|
||||
LoggedInState.NotLoggedIn -> {
|
||||
switchToNotLoggedInFlow(null)
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
||||
|
|
@ -137,20 +143,17 @@ class RootFlowNode(
|
|||
onFailure: () -> Unit,
|
||||
onSuccess: (SessionId) -> Unit,
|
||||
) {
|
||||
matrixSessionCache.getOrRestore(sessionId)
|
||||
.onSuccess {
|
||||
Timber.v("Succeed to restore session $sessionId")
|
||||
onSuccess(sessionId)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to restore session $sessionId")
|
||||
onFailure()
|
||||
}
|
||||
matrixSessionCache.getOrRestore(sessionId).onSuccess {
|
||||
Timber.v("Succeed to restore session $sessionId")
|
||||
onSuccess(sessionId)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to restore session $sessionId")
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun tryToRestoreLatestSession(
|
||||
onSuccess: (SessionId) -> Unit,
|
||||
onFailure: () -> Unit
|
||||
onSuccess: (SessionId) -> Unit, onFailure: () -> Unit
|
||||
) {
|
||||
val latestSessionId = sessionStore.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
|
|
@ -172,32 +175,45 @@ class RootFlowNode(
|
|||
modifier = modifier,
|
||||
onOpenBugReport = this::onOpenBugReport,
|
||||
) {
|
||||
BackstackView()
|
||||
val backstackSlider = rememberBackstackSlider<NavTarget>(
|
||||
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
|
||||
)
|
||||
val backstackFader = rememberBackstackFader<NavTarget>(
|
||||
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
|
||||
)
|
||||
val transitionHandler = rememberDelegateTransitionHandler<NavTarget, BackStack.State> { navTarget ->
|
||||
when (navTarget) {
|
||||
is NavTarget.SplashScreen,
|
||||
is NavTarget.LoggedInFlow -> backstackFader
|
||||
else -> backstackSlider
|
||||
}
|
||||
}
|
||||
BackstackView(transitionHandler = transitionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object SplashScreen : NavTarget
|
||||
@Parcelize data object SplashScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class NotLoggedInFlow(
|
||||
@Parcelize data class AccountSelect(
|
||||
val currentSessionId: SessionId,
|
||||
val intent: Intent?,
|
||||
val permalinkData: PermalinkData?,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize data class NotLoggedInFlow(
|
||||
val params: LoginParams?
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoggedInFlow(
|
||||
val sessionId: SessionId,
|
||||
val navId: Int
|
||||
@Parcelize data class LoggedInFlow(
|
||||
val sessionId: SessionId, val navId: Int
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class SignedOutFlow(
|
||||
@Parcelize data class SignedOutFlow(
|
||||
val sessionId: SessionId
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object BugReport : NavTarget
|
||||
@Parcelize data object BugReport : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -211,6 +227,10 @@ class RootFlowNode(
|
|||
override fun onOpenBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
|
||||
override fun onAddAccount() {
|
||||
backstack.push(NavTarget.NotLoggedInFlow(null))
|
||||
}
|
||||
}
|
||||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
|
|
@ -226,13 +246,11 @@ class RootFlowNode(
|
|||
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
|
||||
}
|
||||
is NavTarget.SignedOutFlow -> {
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
SignedOutEntryPoint.Params(
|
||||
sessionId = navTarget.sessionId
|
||||
)
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext).params(
|
||||
SignedOutEntryPoint.Params(
|
||||
sessionId = navTarget.sessionId
|
||||
)
|
||||
.build()
|
||||
).build()
|
||||
}
|
||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
NavTarget.BugReport -> {
|
||||
|
|
@ -241,10 +259,32 @@ class RootFlowNode(
|
|||
backstack.pop()
|
||||
}
|
||||
}
|
||||
bugReportEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
bugReportEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
|
||||
}
|
||||
is NavTarget.AccountSelect -> {
|
||||
val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback {
|
||||
override fun onSelectAccount(sessionId: SessionId) {
|
||||
lifecycleScope.launch {
|
||||
if (sessionId == navTarget.currentSessionId) {
|
||||
// Ensure that the account selection Node is removed from the backstack
|
||||
// Do not pop when the account is changed to avoid a UI flicker.
|
||||
backstack.pop()
|
||||
}
|
||||
attachSession(sessionId).apply {
|
||||
if (navTarget.intent != null) {
|
||||
attachIncomingShare(navTarget.intent)
|
||||
} else if (navTarget.permalinkData != null) {
|
||||
attachPermalinkData(navTarget.permalinkData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
accountSelectEntryPoint.nodeBuilder(this, buildContext).callback(callback).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -267,19 +307,29 @@ class RootFlowNode(
|
|||
}
|
||||
|
||||
private suspend fun onLoginLink(params: LoginParams) {
|
||||
// Is there a session already?
|
||||
val latestSessionId = sessionStore.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
// No session, open login
|
||||
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
||||
switchToNotLoggedInFlow(params)
|
||||
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
|
||||
// Is there a session already?
|
||||
val sessions = sessionStore.getAllSessions()
|
||||
if (sessions.isNotEmpty()) {
|
||||
if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
|
||||
val loginHintMatrixId = params.loginHint?.removePrefix("mxid:")
|
||||
val existingAccount = sessions.find { it.userId == loginHintMatrixId }
|
||||
if (existingAccount != null) {
|
||||
// We have an existing account matching the login hint, ensure this is the current session
|
||||
sessionStore.setLatestSession(existingAccount.userId)
|
||||
} else {
|
||||
val latestSessionId = sessions.maxBy { it.lastUsageIndex }.userId
|
||||
attachSession(SessionId(latestSessionId))
|
||||
backstack.push(NavTarget.NotLoggedInFlow(params))
|
||||
}
|
||||
} else {
|
||||
Timber.w("Login link ignored, multi account is disabled")
|
||||
}
|
||||
} else {
|
||||
Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
|
||||
switchToNotLoggedInFlow(null)
|
||||
switchToNotLoggedInFlow(params)
|
||||
}
|
||||
} else {
|
||||
// Just ignore the login link if we already have a session
|
||||
Timber.w("Login link ignored, we already have a session")
|
||||
Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,56 +340,95 @@ class RootFlowNode(
|
|||
// No session, open login
|
||||
switchToNotLoggedInFlow(null)
|
||||
} else {
|
||||
attachSession(latestSessionId)
|
||||
.attachIncomingShare(intent)
|
||||
// wait for the current session to be restored
|
||||
val loggedInFlowNode = attachSession(latestSessionId)
|
||||
if (sessionStore.getAllSessions().size > 1) {
|
||||
// Several accounts, let the user choose which one to use
|
||||
backstack.push(
|
||||
NavTarget.AccountSelect(
|
||||
currentSessionId = latestSessionId,
|
||||
intent = intent,
|
||||
permalinkData = null,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Only one account, directly attach the incoming share node.
|
||||
loggedInFlowNode.attachIncomingShare(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun navigateTo(permalinkData: PermalinkData) {
|
||||
Timber.d("Navigating to $permalinkData")
|
||||
attachSession(null)
|
||||
.apply {
|
||||
when (permalinkData) {
|
||||
is PermalinkData.FallbackLink -> Unit
|
||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||
is PermalinkData.RoomLink -> {
|
||||
attachRoom(
|
||||
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||
serverNames = permalinkData.viaParameters,
|
||||
eventId = permalinkData.eventId,
|
||||
clearBackstack = true
|
||||
// Is there a session already?
|
||||
val latestSessionId = sessionStore.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
// No session, open login
|
||||
switchToNotLoggedInFlow(null)
|
||||
} else {
|
||||
// wait for the current session to be restored
|
||||
val loggedInFlowNode = attachSession(latestSessionId)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.FallbackLink -> Unit
|
||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||
else -> {
|
||||
if (sessionStore.getAllSessions().size > 1) {
|
||||
// Several accounts, let the user choose which one to use
|
||||
backstack.push(
|
||||
NavTarget.AccountSelect(
|
||||
currentSessionId = latestSessionId,
|
||||
intent = null,
|
||||
permalinkData = permalinkData,
|
||||
)
|
||||
)
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
attachUser(permalinkData.userId)
|
||||
} else {
|
||||
// Only one account, directly attach the room or the user node.
|
||||
loggedInFlowNode.attachPermalinkData(permalinkData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun LoggedInFlowNode.attachPermalinkData(permalinkData: PermalinkData) {
|
||||
when (permalinkData) {
|
||||
is PermalinkData.FallbackLink -> Unit
|
||||
is PermalinkData.RoomEmailInviteLink -> Unit
|
||||
is PermalinkData.RoomLink -> {
|
||||
attachRoom(
|
||||
roomIdOrAlias = permalinkData.roomIdOrAlias,
|
||||
trigger = JoinedRoom.Trigger.MobilePermalink,
|
||||
serverNames = permalinkData.viaParameters,
|
||||
eventId = permalinkData.eventId,
|
||||
clearBackstack = true
|
||||
)
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
attachUser(permalinkData.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||
Timber.d("Navigating to $deeplinkData")
|
||||
attachSession(deeplinkData.sessionId)
|
||||
.apply {
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
||||
}
|
||||
attachSession(deeplinkData.sessionId).apply {
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias(), clearBackstack = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOidcAction(oidcAction: OidcAction) {
|
||||
oidcActionFlow.post(oidcAction)
|
||||
}
|
||||
|
||||
// [sessionId] will be null for permalink.
|
||||
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
|
||||
// TODO handle multi-session
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||
// Ensure that the session is the latest one
|
||||
sessionStore.setLatestSession(sessionId.value)
|
||||
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
|
||||
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
|
||||
}
|
||||
.attachSession()
|
||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
||||
}.attachSession()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.appnav.di
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class SyncOrchestrator(
|
||||
@Assisted matrixClient: MatrixClient,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoggedInNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||
|
|
@ -64,7 +64,7 @@ import java.util.Optional
|
|||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class RoomFlowNode(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
|
|
@ -45,7 +45,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class JoinedRoomFlowNode(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.appnav.room.RoomNavigationTarget
|
||||
|
|
@ -45,7 +45,7 @@ import kotlinx.parcelize.Parcelize
|
|||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class JoinedRoomLoadedFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class IntentResolverTest {
|
|||
@Test
|
||||
fun `test resolve oidc`() {
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { OidcAction.GoBack },
|
||||
oidcIntentResolverResult = { OidcAction.GoBack() },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -120,7 +120,7 @@ class IntentResolverTest {
|
|||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Oidc(
|
||||
oidcAction = OidcAction.GoBack
|
||||
oidcAction = OidcAction.GoBack()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import dev.zacsweers.metro.BindingContainer
|
|||
import dev.zacsweers.metro.Binds
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.IntoMap
|
||||
import dev.zacsweers.metro.Origin
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
|
||||
|
|
@ -71,14 +72,16 @@ class ContributesNodeProcessor(
|
|||
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
||||
val modulePackage = ksClass.packageName.asString()
|
||||
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
|
||||
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||
val content = FileSpec.builder(
|
||||
packageName = modulePackage,
|
||||
fileName = moduleClassName,
|
||||
)
|
||||
.addType(
|
||||
TypeSpec.interfaceBuilder(moduleClassName)
|
||||
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
|
||||
.addAnnotation(BindingContainer::class)
|
||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.toTypeName()).build())
|
||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
|
||||
.addFunction(
|
||||
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
|
||||
.addModifiers(KModifier.ABSTRACT)
|
||||
|
|
@ -88,7 +91,7 @@ class ContributesNodeProcessor(
|
|||
.addAnnotation(IntoMap::class)
|
||||
.addAnnotation(
|
||||
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
||||
"%T::class",
|
||||
CLASS_PLACEHOLDER,
|
||||
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||
).build()
|
||||
)
|
||||
|
|
@ -115,7 +118,7 @@ class ContributesNodeProcessor(
|
|||
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
||||
if (assistedParameters.size != 2) {
|
||||
error(
|
||||
"${ksClass.qualifiedName?.asString()} must have an @Inject constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
|
||||
"${ksClass.qualifiedName?.asString()} must have a constructor with 2 @Assisted parameters. Found: ${assistedParameters.size}",
|
||||
)
|
||||
}
|
||||
val contextAssistedParam = assistedParameters[0]
|
||||
|
|
@ -138,6 +141,7 @@ class ContributesNodeProcessor(
|
|||
.addType(
|
||||
TypeSpec.interfaceBuilder(assistedFactoryClassName)
|
||||
.addSuperinterface(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(nodeClassName))
|
||||
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember("%T::class", nodeClassName).build())
|
||||
.addAnnotation(AssistedFactory::class)
|
||||
.addFunction(
|
||||
FunSpec.builder("create")
|
||||
|
|
@ -161,6 +165,7 @@ class ContributesNodeProcessor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val CLASS_PLACEHOLDER = "%T::class"
|
||||
private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory")
|
||||
private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appconfig.AnalyticsConfig
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class AnalyticsOptInNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.call.api.CallType
|
||||
|
|
@ -49,7 +49,7 @@ import timber.log.Timber
|
|||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class CallScreenPresenter(
|
||||
@Assisted private val callType: CallType,
|
||||
@Assisted private val navigator: CallScreenNavigator,
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ class WebViewWidgetMessageInterceptor(
|
|||
return assetLoader.shouldInterceptRequest(request.url)
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
||||
return assetLoader.shouldInterceptRequest(url.toUri())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
|||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ChangeRolesNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -47,7 +47,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ChangeRolesPresenter(
|
||||
@Assisted private val role: RoomMember.Role,
|
||||
private val room: JoinedRoom,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appnav.di.RoomComponentFactory
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
|
|
@ -32,7 +32,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ChangeRoomMemberRolesRootNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class CreateRoomFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class AddPeopleNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
|
@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ConfigureRoomNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class AccountDeactivationNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<string name="screen_deactivate_account_list_item_1">"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Véglegesen letiltja"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Eltávolításra kerül az összes csevegőszobából."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai az azonosítási kiszolgálónkról."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket."</string>
|
||||
<string name="screen_deactivate_account_title">"Fiók deaktiválása"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
|||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
|
|
@ -42,7 +43,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class FtueFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class NotificationsOptInNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
|
|
@ -25,7 +25,7 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class NotificationsOptInPresenter(
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
@Assisted private val callback: NotificationsOptInNode.Callback,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
|||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
|
||||
|
|
@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class FtueSessionVerificationFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ChooseSelfVerificationModeNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.permissions.noop)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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.home.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class CurrentUserWithNeighborsBuilder {
|
||||
/**
|
||||
* Build a list of [MatrixUser] containing the current user. If there are other sessions, the list
|
||||
* will contain 3 users, with the current user in the middle.
|
||||
* If there is only one other session, the list will contain twice the other user, to allow cycling.
|
||||
*/
|
||||
fun build(
|
||||
matrixUser: MatrixUser,
|
||||
sessions: List<SessionData>,
|
||||
): ImmutableList<MatrixUser> {
|
||||
// Sort by position to always have the same order (not depending on last account usage)
|
||||
return sessions.sortedBy { it.position }
|
||||
.map {
|
||||
if (it.userId == matrixUser.userId.value) {
|
||||
// Always use the freshest profile for the current user
|
||||
matrixUser
|
||||
} else {
|
||||
// Use the data from the DB
|
||||
MatrixUser(
|
||||
userId = UserId(it.userId),
|
||||
displayName = it.userDisplayName,
|
||||
avatarUrl = it.userAvatarUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
.let { sessionList ->
|
||||
// If the list has one item, there is no other session, return the list
|
||||
when (sessionList.size) {
|
||||
// Can happen when the user signs out (?)
|
||||
0 -> listOf(matrixUser)
|
||||
1 -> sessionList
|
||||
else -> {
|
||||
// Create a list with extra item at the start and end if necessary to have the current user in the middle
|
||||
// If the list is [A, B, C, D] and the current user is A we want to return [D, A, B]
|
||||
// If the current user is B, we want to return [A, B, C]
|
||||
// If the current user is C, we want to return [B, C, D]
|
||||
// If the current user is D, we want to return [C, D, A]
|
||||
// Special case: if there are only two users, we want to return [B, A, B] or [A, B, A] to allows cycling
|
||||
// between the two users.
|
||||
val currentUserIndex = sessionList.indexOfFirst { it.userId == matrixUser.userId }
|
||||
when (currentUserIndex) {
|
||||
// This can happen when the user signs out.
|
||||
// In this case, just return a singleton list with the current user.
|
||||
-1 -> listOf(matrixUser)
|
||||
0 -> listOf(sessionList.last()) + sessionList.take(2)
|
||||
sessionList.lastIndex -> sessionList.takeLast(2) + sessionList.first()
|
||||
else -> sessionList.slice(currentUserIndex - 1..currentUserIndex + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
package io.element.android.features.home.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface HomeEvents {
|
||||
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
|
||||
data class SwitchToAccount(val sessionId: SessionId) : HomeEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
|||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
|
|
@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class HomeFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
|
@ -29,6 +30,10 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
class HomePresenter(
|
||||
|
|
@ -41,10 +46,21 @@ class HomePresenter(
|
|||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val sessionStore: SessionStore,
|
||||
) : Presenter<HomeState> {
|
||||
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
||||
|
||||
@Composable
|
||||
override fun present(): HomeState {
|
||||
val matrixUser = client.userProfile.collectAsState()
|
||||
val coroutineState = rememberCoroutineScope()
|
||||
val matrixUser by client.userProfile.collectAsState()
|
||||
val currentUserAndNeighbors by remember {
|
||||
combine(
|
||||
client.userProfile,
|
||||
sessionStore.sessionsFlow(),
|
||||
currentUserWithNeighborsBuilder::build,
|
||||
)
|
||||
}.collectAsState(initial = persistentListOf(matrixUser))
|
||||
val isOnline by syncService.isOnline.collectAsState()
|
||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||
val roomListState = roomListPresenter.present()
|
||||
|
|
@ -71,6 +87,9 @@ class HomePresenter(
|
|||
is HomeEvents.SelectHomeNavigationBarItem -> {
|
||||
currentHomeNavigationBarItemOrdinal = event.item.ordinal
|
||||
}
|
||||
is HomeEvents.SwitchToAccount -> coroutineState.launch {
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +101,7 @@ class HomePresenter(
|
|||
}
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
return HomeState(
|
||||
matrixUser = matrixUser.value,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
hasNetworkConnection = isOnline,
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,15 @@ import io.element.android.features.home.impl.spaces.HomeSpacesState
|
|||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class HomeState(
|
||||
val matrixUser: MatrixUser,
|
||||
/**
|
||||
* The current user of this session, in case of multiple accounts, will contains 3 items, with the
|
||||
* current user in the middle.
|
||||
*/
|
||||
val currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
val showAvatarIndicator: Boolean,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val currentHomeNavigationBarItem: HomeNavigationBarItem,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
||||
override val values: Sequence<HomeState>
|
||||
|
|
@ -50,6 +51,7 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
|||
|
||||
internal fun aHomeState(
|
||||
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
|
||||
currentUserAndNeighbors: List<MatrixUser> = listOf(matrixUser),
|
||||
showAvatarIndicator: Boolean = false,
|
||||
hasNetworkConnection: Boolean = true,
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
|
|
@ -61,7 +63,7 @@ internal fun aHomeState(
|
|||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||
eventSink: (HomeEvents) -> Unit = {}
|
||||
) = HomeState(
|
||||
matrixUser = matrixUser,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors.toPersistentList(),
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
hasNetworkConnection = hasNetworkConnection,
|
||||
snackbarMessage = snackbarMessage,
|
||||
|
|
|
|||
|
|
@ -171,12 +171,15 @@ private fun HomeScaffold(
|
|||
topBar = {
|
||||
RoomListTopBar(
|
||||
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
|
||||
matrixUser = state.matrixUser,
|
||||
currentUserAndNeighbors = state.currentUserAndNeighbors,
|
||||
showAvatarIndicator = state.showAvatarIndicator,
|
||||
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
|
||||
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onAccountSwitch = {
|
||||
state.eventSink(HomeEvents.SwitchToAccount(it))
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
displayMenuItems = state.displayActions,
|
||||
displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
|
||||
|
|
|
|||
|
|
@ -11,19 +11,25 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
|
@ -41,7 +47,6 @@ import io.element.android.features.home.impl.filters.RoomListFiltersView
|
|||
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient
|
||||
|
|
@ -57,23 +62,29 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListTopBar(
|
||||
title: String,
|
||||
matrixUser: MatrixUser,
|
||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onToggleSearch: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onAccountSwitch: (SessionId) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
displayMenuItems: Boolean,
|
||||
displayFilters: Boolean,
|
||||
|
|
@ -83,10 +94,11 @@ fun RoomListTopBar(
|
|||
) {
|
||||
DefaultRoomListTopBar(
|
||||
title = title,
|
||||
matrixUser = matrixUser,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
areSearchResultsDisplayed = areSearchResultsDisplayed,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onAccountSwitch = onAccountSwitch,
|
||||
onSearchClick = onToggleSearch,
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
|
@ -102,11 +114,12 @@ fun RoomListTopBar(
|
|||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
title: String,
|
||||
matrixUser: MatrixUser,
|
||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
showAvatarIndicator: Boolean,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onOpenSettings: () -> Unit,
|
||||
onAccountSwitch: (SessionId) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
displayMenuItems: Boolean,
|
||||
|
|
@ -116,12 +129,6 @@ private fun DefaultRoomListTopBar(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val collapsedFraction = scrollBehavior.state.collapsedFraction
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
val collapsedTitleTextStyle = ElementTheme.typography.aliasScreenTitle
|
||||
val expandedTitleTextStyle = ElementTheme.typography.fontHeadingLgBold.copy(
|
||||
|
|
@ -158,8 +165,9 @@ private fun DefaultRoomListTopBar(
|
|||
},
|
||||
navigationIcon = {
|
||||
NavigationIcon(
|
||||
avatarData = avatarData,
|
||||
currentUserAndNeighbors = currentUserAndNeighbors,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
onAccountSwitch = onAccountSwitch,
|
||||
onClick = onOpenSettings,
|
||||
)
|
||||
},
|
||||
|
|
@ -247,19 +255,67 @@ private fun DefaultRoomListTopBar(
|
|||
|
||||
@Composable
|
||||
private fun NavigationIcon(
|
||||
avatarData: AvatarData,
|
||||
currentUserAndNeighbors: ImmutableList<MatrixUser>,
|
||||
showAvatarIndicator: Boolean,
|
||||
onAccountSwitch: (SessionId) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (currentUserAndNeighbors.size == 1) {
|
||||
AccountIcon(
|
||||
matrixUser = currentUserAndNeighbors.single(),
|
||||
isCurrentAccount = true,
|
||||
showAvatarIndicator = showAvatarIndicator,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
// Render a vertical pager
|
||||
val pagerState = rememberPagerState(initialPage = 1) { currentUserAndNeighbors.size }
|
||||
// Listen to page changes and switch account if needed
|
||||
val latestOnAccountSwitch by rememberUpdatedState(onAccountSwitch)
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.settledPage }.collect { page ->
|
||||
latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value))
|
||||
}
|
||||
}
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.height(48.dp),
|
||||
) { page ->
|
||||
AccountIcon(
|
||||
matrixUser = currentUserAndNeighbors[page],
|
||||
isCurrentAccount = page == 1,
|
||||
showAvatarIndicator = page == 1 && showAvatarIndicator,
|
||||
onClick = if (page == 1) {
|
||||
onClick
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountIcon(
|
||||
matrixUser: MatrixUser,
|
||||
isCurrentAccount: Boolean,
|
||||
showAvatarIndicator: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
modifier = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Box {
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
contentDescription = stringResource(CommonStrings.common_settings),
|
||||
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
|
||||
)
|
||||
if (showAvatarIndicator) {
|
||||
RedIndicatorAtom(
|
||||
|
|
@ -276,11 +332,12 @@ private fun NavigationIcon(
|
|||
internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
||||
DefaultRoomListTopBar(
|
||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||
showAvatarIndicator = false,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onSearchClick = {},
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
|
|
@ -296,11 +353,33 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
|||
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
|
||||
DefaultRoomListTopBar(
|
||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
|
||||
showAvatarIndicator = true,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onSearchClick = {},
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
filtersState = aRoomListFiltersState(),
|
||||
canReportBug = true,
|
||||
onMenuActionClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DefaultRoomListTopBarMultiAccountPreview() = ElementPreview {
|
||||
DefaultRoomListTopBar(
|
||||
title = stringResource(R.string.screen_roomlist_main_space_title),
|
||||
currentUserAndNeighbors = aMatrixUserList().take(3).toPersistentList(),
|
||||
showAvatarIndicator = false,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onSearchClick = {},
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@
|
|||
<string name="screen_home_tab_chats">"Всички чатове"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Отказване на покана"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Сигурни ли сте, че искате да откажете този личен чат с %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Отказване на чат"</string>
|
||||
<string name="screen_invites_empty_list">"Няма покани"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
|
||||
<string name="screen_migration_message">"Това е еднократен процес, благодаря, че изчакахте."</string>
|
||||
<string name="screen_migration_title">"Настройване на вашия акаунт."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Създаване на нов разговор или стая"</string>
|
||||
<string name="screen_roomlist_empty_message">"Започнете, като изпратите съобщение на някого."</string>
|
||||
<string name="screen_roomlist_empty_title">"Все още няма чатове."</string>
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@
|
|||
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your backup password"</string>
|
||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your backup password?"</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Your message backup is out of sync"</string>
|
||||
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another connected device to access your encrypted messages."</string>
|
||||
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another linked device to access your encrypted messages."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátor-optimalizálását, hogy biztosan megkapja az összes értesítést."</string>
|
||||
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátoroptimalizálását, hogy biztosan megkapja az összes értesítést."</string>
|
||||
<string name="banner_battery_optimization_submit_android">"Optimalizálás letiltása"</string>
|
||||
<string name="banner_battery_optimization_title_android">"Nem érkeznek meg az értesítések?"</string>
|
||||
<string name="banner_set_up_recovery_content">"Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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.home.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import org.junit.Test
|
||||
|
||||
class CurrentUserWithNeighborsBuilderTest {
|
||||
@Test
|
||||
fun `build on empty list returns current user`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser()
|
||||
val list = listOf<SessionData>()
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result).containsExactly(matrixUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure that account are sorted by position`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
position = 3,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
position = 2,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
position = 1,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID_3,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if current user is not found, return a singleton with current user`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one account, will return a singleton`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two accounts, first is current, will return 3 items`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID_2,
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two accounts, second is current, will return 3 items`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `three accounts, first is current, will return last current and next`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID_3,
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `three accounts, second is current, will return first current and last`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID_3,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `three accounts, current is last, will return middle, current and first`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(id = A_USER_ID_3.value)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_2.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID_3.value,
|
||||
),
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result.map { it.userId }).containsExactly(
|
||||
A_USER_ID,
|
||||
A_USER_ID_2,
|
||||
A_USER_ID_3,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one account, will return data from matrix user and not from db`() {
|
||||
val sut = CurrentUserWithNeighborsBuilder()
|
||||
val matrixUser = aMatrixUser(
|
||||
id = A_USER_ID.value,
|
||||
displayName = "Bob",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
val list = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_USER_ID.value,
|
||||
userDisplayName = "Outdated Bob",
|
||||
userAvatarUrl = "outdatedAvatarUrl",
|
||||
),
|
||||
)
|
||||
val result = sut.build(matrixUser, list)
|
||||
assertThat(result).containsExactly(
|
||||
MatrixUser(
|
||||
userId = A_USER_ID,
|
||||
displayName = "Bob",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
|||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
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.MutablePresenter
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
|
|
@ -54,17 +57,29 @@ class HomePresenterTest {
|
|||
val presenter = createHomePresenter(
|
||||
client = matrixClient,
|
||||
rageshakeFeatureAvailability = { flowOf(false) },
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = matrixClient.sessionId.value,
|
||||
userDisplayName = null,
|
||||
userAvatarUrl = null,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
|
||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
||||
MatrixUser(A_USER_ID, null, null)
|
||||
)
|
||||
assertThat(initialState.canReportBug).isFalse()
|
||||
skipItems(1)
|
||||
val withUserState = awaitItem()
|
||||
assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
||||
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(withUserState.currentUserAndNeighbors.first()).isEqualTo(
|
||||
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
|
||||
)
|
||||
assertThat(withUserState.showAvatarIndicator).isFalse()
|
||||
assertThat(withUserState.isSpaceFeatureEnabled).isFalse()
|
||||
assertThat(withUserState.showNavigationBar).isFalse()
|
||||
|
|
@ -75,6 +90,9 @@ class HomePresenterTest {
|
|||
fun `present - can report bug`() = runTest {
|
||||
val presenter = createHomePresenter(
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -92,6 +110,9 @@ class HomePresenterTest {
|
|||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
||||
),
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
|
|
@ -105,6 +126,9 @@ class HomePresenterTest {
|
|||
val indicatorService = FakeIndicatorService()
|
||||
val presenter = createHomePresenter(
|
||||
indicatorService = indicatorService,
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -124,19 +148,28 @@ class HomePresenterTest {
|
|||
userAvatarUrl = null,
|
||||
)
|
||||
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
|
||||
val presenter = createHomePresenter(client = matrixClient)
|
||||
val presenter = createHomePresenter(
|
||||
client = matrixClient,
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||
// No new state is coming
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - NavigationBar change`() = runTest {
|
||||
val presenter = createHomePresenter()
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -152,6 +185,9 @@ class HomePresenterTest {
|
|||
fun `present - NavigationBar is hidden when the last space is left`() = runTest {
|
||||
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
|
||||
val presenter = createHomePresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
||||
),
|
||||
|
|
@ -185,6 +221,7 @@ internal fun createHomePresenter(
|
|||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = HomePresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
|
|
@ -195,4 +232,5 @@ internal fun createHomePresenter(
|
|||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DeclineAndBlockNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.impl.DeclineInvite
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DeclineAndBlockPresenter(
|
||||
@Assisted private val inviteData: InviteData,
|
||||
private val declineInvite: DeclineInvite,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
<string name="screen_decline_and_block_block_user_option_title">"Блокиране на потребителя"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Отказване на покана"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Сигурни ли сте, че искате да откажете този личен чат с %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Отказване на чат"</string>
|
||||
<string name="screen_invites_empty_list">"Няма покани"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<string name="screen_decline_and_block_block_user_option_title">"Felhasználó letiltása"</string>
|
||||
<string name="screen_decline_and_block_report_user_option_description">"A szoba jelentése a fiókszolgáltatójának."</string>
|
||||
<string name="screen_decline_and_block_report_user_reason_placeholder">"Írja le a jelentés okát…"</string>
|
||||
<string name="screen_decline_and_block_title">"Elutasítás és blokkolás"</string>
|
||||
<string name="screen_decline_and_block_title">"Elutasítás és letiltás"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Meghívás elutasítása"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"</string>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
|
|
@ -51,7 +51,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DefaultInvitePeoplePresenter(
|
||||
@Assisted private val joinedRoom: JoinedRoom?,
|
||||
@Assisted private val roomId: RoomId,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class JoinRoomFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
|
|
@ -59,7 +59,7 @@ import kotlinx.coroutines.launch
|
|||
import java.util.Optional
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class JoinRoomPresenter(
|
||||
@Assisted private val roomId: RoomId,
|
||||
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"Tato místnost je buď určena pouze pro zvané, nebo do ní může být omezen přístup na úrovni prostoru."</string>
|
||||
<string name="screen_join_room_forget_action">"Zapomenout na tuto místnost"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Abyste se mohli připojit k této místnosti, potřebujete pozvánku."</string>
|
||||
<string name="screen_join_room_invited_by">"Pozván(a)"</string>
|
||||
<string name="screen_join_room_join_action">"Připojit se do místnosti"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Abyste se mohli připojit, musíte být pozváni nebo být členem některého prostoru."</string>
|
||||
<string name="screen_join_room_knock_action">"Zaklepejte a připojte se"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"Dette rum er enten kun for gæster, eller der kan være sat begrænsninger for adgangen på klyngeniveau."</string>
|
||||
<string name="screen_join_room_forget_action">"Glem dette rum"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Du har brug for en invitation for at deltage i dette rum"</string>
|
||||
<string name="screen_join_room_invited_by">"Inviteret af"</string>
|
||||
<string name="screen_join_room_join_action">"Deltag i rummet"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Du skal muligvis være inviteret eller være medlem af en klynge for at deltage."</string>
|
||||
<string name="screen_join_room_knock_action">"Send anmodning om at deltage"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"Dieser Chat ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces."</string>
|
||||
<string name="screen_join_room_forget_action">"Vergiss diesen Chat"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Du benötigst eine Einladung, um diesem Chat beizutreten"</string>
|
||||
<string name="screen_join_room_invited_by">"Eingeladen von"</string>
|
||||
<string name="screen_join_room_join_action">"Chat beitreten"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Möglicherweise musst du eingeladen werden oder ein Mitglied eines Spaces sein, um beitreten zu können."</string>
|
||||
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud."</string>
|
||||
<string name="screen_join_room_forget_action">"Unusta see jututuba"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Selle jututoaga liitumiseks vajad sa kutset"</string>
|
||||
<string name="screen_join_room_invited_by">"Kutsuja"</string>
|
||||
<string name="screen_join_room_join_action">"Liitu jututoaga"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Selle jututoaga liitumiseks sa vajad kutset või pead juba olema kogukonna liige."</string>
|
||||
<string name="screen_join_room_knock_action">"Liitumiseks koputa jututoa uksele"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions d’accès au niveau de l’espace."</string>
|
||||
<string name="screen_join_room_forget_action">"Oublier ce salon"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Vous avez besoin d’une invitation pour rejoindre ce salon"</string>
|
||||
<string name="screen_join_room_invited_by">"Invité(e) par"</string>
|
||||
<string name="screen_join_room_join_action">"Rejoindre"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Il est possible que vous deviez être invité ou être membre d’un Espace pour pouvoir rejoindre le salon."</string>
|
||||
<string name="screen_join_room_knock_action">"Demander à joindre"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."</string>
|
||||
<string name="screen_join_room_forget_action">"Szoba elfelejtése"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Meghívóra van szüksége ahhoz, hogy csatlakozzon ehhez a szobához"</string>
|
||||
<string name="screen_join_room_invited_by">"Meghívta:"</string>
|
||||
<string name="screen_join_room_join_action">"Csatlakozás a szobához"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"A csatlakozáshoz meghívásra vagy tértagságra lehet szüksége."</string>
|
||||
<string name="screen_join_room_knock_action">"Kopogtasson a csatlakozáshoz"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="screen_join_room_fail_reason">"A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço."</string>
|
||||
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
|
||||
<string name="screen_join_room_invite_required_message">"Precisas de um convite para entrares nesta sala"</string>
|
||||
<string name="screen_join_room_invited_by">"Convidado por"</string>
|
||||
<string name="screen_join_room_join_action">"Entrar na sala"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar."</string>
|
||||
<string name="screen_join_room_knock_action">"Bater à porta"</string>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class KnockRequestsListNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="leave_room_alert_empty_subtitle">"Сигурни ли сте, че искате да напуснете тази стая? Вие сте единственият човек тук. Ако напуснете, никой няма да може да се присъедини в бъдеще, включително и вие."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Сигурни ли сте, че искате да напуснете тази стая? Тази стая не е общодостъпна и няма да можете да се присъедините отново без покана."</string>
|
||||
<string name="leave_room_alert_subtitle">"Сигурни ли сте, че искате да напуснете стаята?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
|||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.details.DependenciesDetailsNode
|
||||
import io.element.android.features.licenses.impl.list.DependencyLicensesListNode
|
||||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.architecture.createNode
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DependenciesFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DependenciesDetailsNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DependencyLicensesListNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
|||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
@Suppress("unused")
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class DefaultPermissionsPresenter(
|
||||
@Assisted private val permissions: List<String>
|
||||
) : PermissionsPresenter {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class SendLocationNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||
|
|
@ -36,7 +36,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
|||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class SendLocationPresenter(
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val room: JoinedRoom,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
|
|
@ -23,7 +23,7 @@ import io.element.android.libraries.di.RoomScope
|
|||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ShowLocationNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ShowLocationPresenter(
|
||||
@Assisted private val location: Location,
|
||||
@Assisted private val description: String?,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
|
||||
|
|
@ -29,7 +29,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LockScreenFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
|||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
|
|
@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LockScreenSettingsFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LockScreenSettingsNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
|
||||
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
|
||||
|
|
@ -32,7 +32,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LockScreenSetupFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class SetupBiometricNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class SetupPinNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class PinUnlockNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_app_lock_biometric_authentication">"биометрично удостоверяване"</string>
|
||||
<string name="screen_app_lock_biometric_unlock">"биометрично отключване"</string>
|
||||
<string name="screen_app_lock_biometric_unlock_title_android">"Отключване с биометрия"</string>
|
||||
<string name="screen_app_lock_confirm_biometric_authentication_android">"Потвърдете биометричните данни"</string>
|
||||
<string name="screen_app_lock_forgot_pin">"Забравихте PIN?"</string>
|
||||
<string name="screen_app_lock_settings_change_pin">"Промяна на PIN кода"</string>
|
||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешаване на биометрично отключване"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin">"Премахване на PIN"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Сигурни ли сте, че искате да премахнете PIN?"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Премахване на PIN?"</string>
|
||||
|
|
@ -9,6 +14,10 @@
|
|||
<string name="screen_app_lock_setup_biometric_unlock_skip">"Предпочитам да използвам PIN"</string>
|
||||
<string name="screen_app_lock_setup_choose_pin">"Избор на PIN"</string>
|
||||
<string name="screen_app_lock_setup_confirm_pin">"Потвърждаване на PIN"</string>
|
||||
<string name="screen_app_lock_setup_pin_context">"Заключете %1$s, за да добавите допълнителна сигурност към вашите чатове.
|
||||
|
||||
Изберете нещо запомнящо се. Ако забравите този PIN, ще бъдете излезли от приложението."</string>
|
||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Не можете да изберете това за ваш PIN код от съображения за сигурност"</string>
|
||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Избор на различен PIN"</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Моля, въведете един и същ PIN два пъти"</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs не съвпадат"</string>
|
||||
|
|
@ -20,6 +29,7 @@
|
|||
<item quantity="one">"Грешен PIN. Имате още %1$d шанс"</item>
|
||||
<item quantity="other">"Грешен PIN. Имате още %1$d шанса"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"Използване на биометрия"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"Използване на PIN"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Излизане…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ dependencies {
|
|||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
|
|
@ -56,5 +57,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
|
|
@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoginFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
@ -87,7 +87,7 @@ class LoginFlowNode(
|
|||
// by pressing back or by closing the Custom Chrome Tab.
|
||||
lifecycleScope.launch {
|
||||
delay(5000)
|
||||
oidcActionFlow.post(OidcAction.GoBack)
|
||||
oidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,9 +94,14 @@ class LoginHelper(
|
|||
}
|
||||
|
||||
private suspend fun onOidcAction(oidcAction: OidcAction) {
|
||||
if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
|
||||
// Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
|
||||
// This can happen if there is an error, for instance attempt to login again on the same account.
|
||||
return
|
||||
}
|
||||
loginModeState.value = AsyncData.Loading()
|
||||
when (oidcAction) {
|
||||
OidcAction.GoBack -> {
|
||||
is OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
.onSuccess {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.error.ChangeServerErrorProvider
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.libraries.androidutils.system.openGooglePlay
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
|
@ -23,6 +22,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
|
|
@ -89,6 +89,12 @@ fun LoginModeView(
|
|||
onSubmit = onClearError,
|
||||
)
|
||||
}
|
||||
is AuthenticationException.AccountAlreadyLoggedIn -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_account_already_logged_in, error.message.orEmpty()),
|
||||
onSubmit = onClearError,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
|
|
@ -113,7 +119,7 @@ fun LoginModeView(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LoginModeViewPreview(@PreviewParameter(ChangeServerErrorProvider::class) error: ChangeServerError) {
|
||||
internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::class) error: Throwable) {
|
||||
ElementPreview {
|
||||
LoginModeView(
|
||||
loginMode = AsyncData.Failure(error),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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.login.impl.login
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.error.ChangeServerErrorProvider
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
|
||||
class LoginModeViewErrorProvider : PreviewParameterProvider<Exception> {
|
||||
override val values: Sequence<Exception>
|
||||
get() = ChangeServerErrorProvider().values +
|
||||
AuthenticationException.AccountAlreadyLoggedIn("@alice:matrix.org")
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginBindings
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginGraph
|
||||
|
|
@ -49,7 +49,7 @@ import kotlinx.parcelize.Parcelize
|
|||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class QrCodeLoginFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ChangeAccountProviderNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ChooseAccountProviderNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ConfirmAccountProviderNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class ConfirmAccountProviderPresenter(
|
||||
@Assisted private val params: Params,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.NodeInputs
|
|||
import io.element.android.libraries.architecture.inputs
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class CreateAccountNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
|
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class CreateAccountPresenter(
|
||||
@Assisted private val url: String,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoginPasswordNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class OnBoardingNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
@ -97,6 +97,7 @@ class OnBoardingNode(
|
|||
onNeedLoginPassword = ::onLoginPasswordNeeded,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = ::onCreateAccountContinue,
|
||||
onBackClick = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||
|
|
@ -27,9 +27,10 @@ import io.element.android.features.login.impl.login.LoginHelper
|
|||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class OnBoardingPresenter(
|
||||
@Assisted private val params: OnBoardingNode.Params,
|
||||
private val buildMeta: BuildMeta,
|
||||
|
|
@ -38,6 +39,7 @@ class OnBoardingPresenter(
|
|||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val loginHelper: LoginHelper,
|
||||
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
|
||||
private val sessionStore: SessionStore,
|
||||
) : Presenter<OnBoardingState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -86,6 +88,10 @@ class OnBoardingPresenter(
|
|||
val onBoardingLogoResId = remember {
|
||||
onBoardingLogoResIdProvider.get()
|
||||
}
|
||||
val isAddingAccount by produceState(initialValue = false) {
|
||||
// We are adding an account if there is at least one session already stored
|
||||
value = sessionStore.getAllSessions().isNotEmpty()
|
||||
}
|
||||
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
|
||||
|
|
@ -109,6 +115,7 @@ class OnBoardingPresenter(
|
|||
}
|
||||
|
||||
return OnBoardingState(
|
||||
isAddingAccount = isAddingAccount,
|
||||
productionApplicationName = buildMeta.productionApplicationName,
|
||||
defaultAccountProvider = defaultAccountProvider,
|
||||
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.features.login.impl.login.LoginMode
|
|||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class OnBoardingState(
|
||||
val isAddingAccount: Boolean,
|
||||
val productionApplicationName: String,
|
||||
val defaultAccountProvider: String?,
|
||||
val mustChooseAccountProvider: Boolean,
|
||||
|
|
|
|||
|
|
@ -23,10 +23,16 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
|||
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true),
|
||||
anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true),
|
||||
anOnBoardingState(customLogoResId = R.drawable.sample_background),
|
||||
anOnBoardingState(
|
||||
isAddingAccount = true,
|
||||
canLoginWithQrCode = true,
|
||||
canCreateAccount = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun anOnBoardingState(
|
||||
isAddingAccount: Boolean = false,
|
||||
productionApplicationName: String = "Element",
|
||||
defaultAccountProvider: String? = null,
|
||||
mustChooseAccountProvider: Boolean = false,
|
||||
|
|
@ -39,6 +45,7 @@ fun anOnBoardingState(
|
|||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
eventSink: (OnBoardingEvents) -> Unit = {},
|
||||
) = OnBoardingState(
|
||||
isAddingAccount = isAddingAccount,
|
||||
productionApplicationName = productionApplicationName,
|
||||
defaultAccountProvider = defaultAccountProvider,
|
||||
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
|
|
@ -58,6 +60,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun OnBoardingView(
|
||||
state: OnBoardingState,
|
||||
onBackClick: () -> Unit,
|
||||
onSignInWithQrCode: () -> Unit,
|
||||
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
|
|
@ -67,6 +70,52 @@ fun OnBoardingView(
|
|||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
onReportProblem: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val loginView = @Composable {
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
onClearError = {
|
||||
state.eventSink(OnBoardingEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
}
|
||||
val buttons = @Composable {
|
||||
OnBoardingButtons(
|
||||
state = state,
|
||||
onSignInWithQrCode = onSignInWithQrCode,
|
||||
onSignIn = onSignIn,
|
||||
onCreateAccount = onCreateAccount,
|
||||
onReportProblem = onReportProblem,
|
||||
)
|
||||
}
|
||||
|
||||
if (state.isAddingAccount) {
|
||||
AddOtherAccountScaffold(
|
||||
modifier = modifier,
|
||||
loginView = loginView,
|
||||
buttons = buttons,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
} else {
|
||||
AddFirstAccountScaffold(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
loginView = loginView,
|
||||
buttons = buttons,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddFirstAccountScaffold(
|
||||
state: OnBoardingState,
|
||||
loginView: @Composable () -> Unit,
|
||||
buttons: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnBoardingPage(
|
||||
modifier = modifier,
|
||||
|
|
@ -79,29 +128,31 @@ fun OnBoardingView(
|
|||
} else {
|
||||
OnBoardingContent(state = state)
|
||||
}
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
onClearError = {
|
||||
state.eventSink(OnBoardingEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
loginView()
|
||||
},
|
||||
footer = {
|
||||
OnBoardingButtons(
|
||||
state = state,
|
||||
onSignInWithQrCode = onSignInWithQrCode,
|
||||
onSignIn = onSignIn,
|
||||
onCreateAccount = onCreateAccount,
|
||||
onReportProblem = onReportProblem,
|
||||
)
|
||||
buttons()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddOtherAccountScaffold(
|
||||
loginView: @Composable () -> Unit,
|
||||
buttons: @Composable () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
title = stringResource(CommonStrings.common_add_account),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()),
|
||||
buttons = { buttons() },
|
||||
content = loginView,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnBoardingContent(state: OnBoardingState) {
|
||||
Box(
|
||||
|
|
@ -226,27 +277,29 @@ private fun OnBoardingButtons(
|
|||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
if (state.canReportBug) {
|
||||
// Add a report problem text button. Use a Text since we need a special theme here.
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onReportProblem)
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = CommonStrings.common_report_a_problem),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
state.eventSink(OnBoardingEvents.OnVersionClick)
|
||||
}
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = R.string.screen_onboarding_app_version, state.version),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
if (state.isAddingAccount.not()) {
|
||||
if (state.canReportBug) {
|
||||
// Add a report problem text button. Use a Text since we need a special theme here.
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onReportProblem)
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = CommonStrings.common_report_a_problem),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
state.eventSink(OnBoardingEvents.OnVersionClick)
|
||||
}
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = R.string.screen_onboarding_app_version, state.version),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -258,6 +311,7 @@ internal fun OnBoardingViewPreview(
|
|||
) = ElementPreview {
|
||||
OnBoardingView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onSignInWithQrCode = {},
|
||||
onSignIn = {},
|
||||
onCreateAccount = {},
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class QrCodeConfirmationNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
||||
|
|
@ -22,7 +22,7 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class QrCodeErrorNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class QrCodeIntroNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class QrCodeScanNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue