Merge pull request #5641 from element-hq/feature/bma/callbackRenaming

Improve architecture around Nodes
This commit is contained in:
Benoit Marty 2025-10-31 13:42:39 +01:00 committed by GitHub
commit f88c2b7d69
260 changed files with 2804 additions and 2675 deletions

View file

@ -59,6 +59,7 @@ dependencies {
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.features.forward.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)

View file

@ -21,13 +21,13 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.SessionGraphFactory import io.element.android.appnav.di.SessionGraphFactory
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.DependencyInjectionGraphOwner
@ -56,10 +56,12 @@ class LoggedInAppScopeFlowNode(
plugins = plugins plugins = plugins
), DependencyInjectionGraphOwner { ), DependencyInjectionGraphOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun navigateToBugReport()
fun onAddAccount() fun navigateToAddAccount()
} }
private val callback: Callback = callback()
@Parcelize @Parcelize
object NavTarget : Parcelable object NavTarget : Parcelable
@ -81,12 +83,12 @@ class LoggedInAppScopeFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
val callback = object : LoggedInFlowNode.Callback { val callback = object : LoggedInFlowNode.Callback {
override fun onOpenBugReport() { override fun navigateToBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } callback.navigateToBugReport()
} }
override fun onAddAccount() { override fun navigateToAddAccount() {
plugins<Callback>().forEach { it.onAddAccount() } callback.navigateToAddAccount()
} }
} }
return createNode<LoggedInFlowNode>(buildContext, listOf(callback)) return createNode<LoggedInFlowNode>(buildContext, listOf(callback))

View file

@ -24,7 +24,6 @@ import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.BackStack.State.ACTIVE import com.bumble.appyx.navmodel.backstack.BackStack.State.ACTIVE
import com.bumble.appyx.navmodel.backstack.BackStack.State.CREATED import com.bumble.appyx.navmodel.backstack.BackStack.State.CREATED
@ -67,6 +66,7 @@ import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.architecture.waitForNavTargetAttached import io.element.android.libraries.architecture.waitForNavTargetAttached
@ -148,10 +148,11 @@ class LoggedInFlowNode(
plugins = plugins plugins = plugins
) { ) {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun navigateToBugReport()
fun onAddAccount() fun navigateToAddAccount()
} }
private val callback: Callback = callback()
private val loggedInFlowProcessor = LoggedInEventProcessor( private val loggedInFlowProcessor = LoggedInEventProcessor(
snackbarDispatcher = snackbarDispatcher, snackbarDispatcher = snackbarDispatcher,
roomMembershipObserver = matrixClient.roomMembershipObserver, roomMembershipObserver = matrixClient.roomMembershipObserver,
@ -282,7 +283,7 @@ class LoggedInFlowNode(
data object Ftue : NavTarget data object Ftue : NavTarget
@Parcelize @Parcelize
data object RoomDirectorySearch : NavTarget data object RoomDirectory : NavTarget
@Parcelize @Parcelize
data class IncomingShare(val intent: Intent) : NavTarget data class IncomingShare(val intent: Intent) : NavTarget
@ -304,46 +305,47 @@ class LoggedInFlowNode(
} }
NavTarget.Home -> { NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback { val callback = object : HomeEntryPoint.Callback {
override fun onRoomClick(roomId: RoomId) { override fun navigateToRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
override fun onSettingsClick() { override fun navigateToSettings() {
backstack.push(NavTarget.Settings()) backstack.push(NavTarget.Settings())
} }
override fun onStartChatClick() { override fun navigateToCreateRoom() {
backstack.push(NavTarget.CreateRoom) backstack.push(NavTarget.CreateRoom)
} }
override fun onSetUpRecoveryClick() { override fun navigateToSetUpRecovery() {
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root)) backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
} }
override fun onSessionConfirmRecoveryKeyClick() { override fun navigateToEnterRecoveryKey() {
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
} }
override fun onRoomSettingsClick(roomId: RoomId) { override fun navigateToRoomSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Details))
} }
override fun onReportBugClick() { override fun navigateToBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } callback.navigateToBugReport()
} }
} }
homeEntryPoint homeEntryPoint.createNode(
.nodeBuilder(this, buildContext) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() callback = callback,
)
} }
is NavTarget.Room -> { is NavTarget.Room -> {
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback { val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) { override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames))
} }
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
when (data) { when (data) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
// Should not happen (handled by MessagesNode) // Should not happen (handled by MessagesNode)
@ -369,7 +371,7 @@ class LoggedInFlowNode(
} }
} }
override fun onOpenGlobalNotificationSettings() { override fun navigateToGlobalNotificationSettings() {
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
} }
} }
@ -384,76 +386,85 @@ class LoggedInFlowNode(
} }
is NavTarget.UserProfile -> { is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback { val callback = object : UserProfileEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) { override fun navigateToRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
} }
userProfileEntryPoint.nodeBuilder(this, buildContext) userProfileEntryPoint.createNode(
.params(UserProfileEntryPoint.Params(userId = navTarget.userId)) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = UserProfileEntryPoint.Params(userId = navTarget.userId),
callback = callback,
)
} }
is NavTarget.Settings -> { is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback { val callback = object : PreferencesEntryPoint.Callback {
override fun onAddAccount() { override fun navigateToAddAccount() {
plugins<Callback>().forEach { it.onAddAccount() } callback.navigateToAddAccount()
} }
override fun onOpenBugReport() { override fun navigateToBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } callback.navigateToAddAccount()
} }
override fun onSecureBackupClick() { override fun navigateToSecureBackup() {
backstack.push(NavTarget.SecureBackup()) backstack.push(NavTarget.SecureBackup())
} }
override fun onOpenRoomNotificationSettings(roomId: RoomId) { override fun navigateToRoomNotificationSettings(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings)) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
} }
override fun navigateTo(roomId: RoomId, eventId: EventId) { override fun navigateToEvent(roomId: RoomId, eventId: EventId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Root(eventId))) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Root(eventId)))
} }
} }
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
preferencesEntryPoint.nodeBuilder(this, buildContext) preferencesEntryPoint.createNode(
.params(inputs) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = inputs,
callback = callback,
)
} }
NavTarget.CreateRoom -> { NavTarget.CreateRoom -> {
val callback = object : StartChatEntryPoint.Callback { val callback = object : StartChatEntryPoint.Callback {
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) { override fun onRoomCreated(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames)) backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames))
} }
override fun onOpenRoomDirectory() { override fun navigateToRoomDirectory() {
backstack.push(NavTarget.RoomDirectorySearch) backstack.push(NavTarget.RoomDirectory)
} }
} }
startChatEntryPoint startChatEntryPoint.createNode(
.nodeBuilder(this, buildContext) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() callback = callback,
)
} }
is NavTarget.SecureBackup -> { is NavTarget.SecureBackup -> {
secureBackupEntryPoint.nodeBuilder(this, buildContext) secureBackupEntryPoint.createNode(
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement)) parentNode = this,
.callback(object : SecureBackupEntryPoint.Callback { buildContext = buildContext,
params = SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement),
callback = object : SecureBackupEntryPoint.Callback {
override fun onDone() { override fun onDone() {
backstack.pop() backstack.pop()
} }
}) },
.build() )
} }
NavTarget.Ftue -> { NavTarget.Ftue -> {
ftueEntryPoint.createNode(this, buildContext) ftueEntryPoint.createNode(this, buildContext)
} }
NavTarget.RoomDirectorySearch -> { NavTarget.RoomDirectory -> {
roomDirectoryEntryPoint.nodeBuilder(this, buildContext) roomDirectoryEntryPoint.createNode(
.callback(object : RoomDirectoryEntryPoint.Callback { parentNode = this,
override fun onResultClick(roomDescription: RoomDescription) { buildContext = buildContext,
callback = object : RoomDirectoryEntryPoint.Callback {
override fun navigateToRoom(roomDescription: RoomDescription) {
backstack.push( backstack.push(
NavTarget.Room( NavTarget.Room(
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(), roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
@ -462,31 +473,35 @@ class LoggedInFlowNode(
) )
) )
} }
}) },
.build() )
} }
is NavTarget.IncomingShare -> { is NavTarget.IncomingShare -> {
shareEntryPoint.nodeBuilder(this, buildContext) shareEntryPoint.createNode(
.callback(object : ShareEntryPoint.Callback { parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(intent = navTarget.intent),
callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) { override fun onDone(roomIds: List<RoomId>) {
navigateUp() navigateUp()
roomIds.singleOrNull()?.let { roomId -> roomIds.singleOrNull()?.let { roomId ->
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
} }
} }
}) },
.params(ShareEntryPoint.Params(intent = navTarget.intent)) )
.build()
} }
is NavTarget.IncomingVerificationRequest -> { is NavTarget.IncomingVerificationRequest -> {
incomingVerificationEntryPoint.nodeBuilder(this, buildContext) incomingVerificationEntryPoint.createNode(
.params(IncomingVerificationEntryPoint.Params(navTarget.data)) parentNode = this,
.callback(object : IncomingVerificationEntryPoint.Callback { buildContext = buildContext,
params = IncomingVerificationEntryPoint.Params(navTarget.data),
callback = object : IncomingVerificationEntryPoint.Callback {
override fun onDone() { override fun onDone() {
backstack.pop() backstack.pop()
} }
}) },
.build() )
} }
} }
} }

View file

@ -18,7 +18,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
@ -29,6 +28,7 @@ import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation import io.element.android.libraries.designsystem.utils.ScreenOrientation
@ -55,9 +55,10 @@ class NotLoggedInFlowNode(
) : NodeInputs ) : NodeInputs
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun navigateToBugReport()
} }
private val callback: Callback = callback()
private val inputs = inputs<Params>() private val inputs = inputs<Params>()
override fun onBuilt() { override fun onBuilt() {
@ -78,20 +79,19 @@ class NotLoggedInFlowNode(
return when (navTarget) { return when (navTarget) {
NavTarget.Root -> { NavTarget.Root -> {
val callback = object : LoginEntryPoint.Callback { val callback = object : LoginEntryPoint.Callback {
override fun onReportProblem() { override fun navigateToBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } callback.navigateToBugReport()
} }
} }
loginEntryPoint loginEntryPoint.createNode(
.nodeBuilder(this, buildContext) parentNode = this,
.params( buildContext = buildContext,
LoginEntryPoint.Params( params = LoginEntryPoint.Params(
accountProvider = inputs.loginParams?.accountProvider, accountProvider = inputs.loginParams?.accountProvider,
loginHint = inputs.loginParams?.loginHint, loginHint = inputs.loginParams?.loginHint,
) ),
) callback = callback,
.callback(callback) )
.build()
} }
} }
} }

View file

@ -227,11 +227,11 @@ class RootFlowNode(
} }
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
val callback = object : LoggedInAppScopeFlowNode.Callback { val callback = object : LoggedInAppScopeFlowNode.Callback {
override fun onOpenBugReport() { override fun navigateToBugReport() {
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
override fun onAddAccount() { override fun navigateToAddAccount() {
backstack.push(NavTarget.NotLoggedInFlow(null)) backstack.push(NavTarget.NotLoggedInFlow(null))
} }
} }
@ -239,7 +239,7 @@ class RootFlowNode(
} }
is NavTarget.NotLoggedInFlow -> { is NavTarget.NotLoggedInFlow -> {
val callback = object : NotLoggedInFlowNode.Callback { val callback = object : NotLoggedInFlowNode.Callback {
override fun onOpenBugReport() { override fun navigateToBugReport() {
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
} }
@ -249,11 +249,13 @@ class RootFlowNode(
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback)) createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
} }
is NavTarget.SignedOutFlow -> { is NavTarget.SignedOutFlow -> {
signedOutEntryPoint.nodeBuilder(this, buildContext).params( signedOutEntryPoint.createNode(
SignedOutEntryPoint.Params( parentNode = this,
sessionId = navTarget.sessionId buildContext = buildContext,
) params = SignedOutEntryPoint.Params(
).build() sessionId = navTarget.sessionId,
),
)
} }
NavTarget.SplashScreen -> emptyNode(buildContext) NavTarget.SplashScreen -> emptyNode(buildContext)
NavTarget.BugReport -> { NavTarget.BugReport -> {
@ -262,11 +264,15 @@ class RootFlowNode(
backstack.pop() backstack.pop()
} }
} }
bugReportEntryPoint.nodeBuilder(this, buildContext).callback(callback).build() bugReportEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
callback = callback,
)
} }
is NavTarget.AccountSelect -> { is NavTarget.AccountSelect -> {
val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback { val callback: AccountSelectEntryPoint.Callback = object : AccountSelectEntryPoint.Callback {
override fun onSelectAccount(sessionId: SessionId) { override fun onAccountSelected(sessionId: SessionId) {
lifecycleScope.launch { lifecycleScope.launch {
if (sessionId == navTarget.currentSessionId) { if (sessionId == navTarget.currentSessionId) {
// Ensure that the account selection Node is removed from the backstack // Ensure that the account selection Node is removed from the backstack
@ -287,7 +293,11 @@ class RootFlowNode(
backstack.pop() backstack.pop()
} }
} }
accountSelectEntryPoint.nodeBuilder(this, buildContext).callback(callback).build() accountSelectEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
callback = callback,
)
} }
} }
} }

View file

@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -32,18 +32,14 @@ class LoggedInNode(
fun navigateToNotificationTroubleshoot() fun navigateToNotificationTroubleshoot()
} }
private fun navigateToNotificationTroubleshoot() { private val callback: Callback = callback()
plugins<Callback>().forEach {
it.navigateToNotificationTroubleshoot()
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val loggedInState = loggedInPresenter.present() val loggedInState = loggedInPresenter.present()
LoggedInView( LoggedInView(
state = loggedInState, state = loggedInState,
navigateToNotificationTroubleshoot = ::navigateToNotificationTroubleshoot, navigateToNotificationTroubleshoot = callback::navigateToNotificationTroubleshoot,
modifier = modifier modifier = modifier
) )
} }

View file

@ -180,10 +180,12 @@ class RoomFlowNode(
} }
} }
val params = Params(navTarget.roomAlias) val params = Params(navTarget.roomAlias)
roomAliasResolverEntryPoint.nodeBuilder(this, buildContext) roomAliasResolverEntryPoint.createNode(
.callback(callback) parentNode = this,
.params(params) buildContext = buildContext,
.build() params = params,
callback = callback,
)
} }
is NavTarget.JoinRoom -> { is NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs( val inputs = JoinRoomEntryPoint.Inputs(
@ -193,7 +195,11 @@ class RoomFlowNode(
serverNames = navTarget.serverNames, serverNames = navTarget.serverNames,
trigger = navTarget.trigger, trigger = navTarget.trigger,
) )
joinRoomEntryPoint.createNode(this, buildContext, inputs) joinRoomEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
inputs = inputs,
)
} }
is NavTarget.JoinedRoom -> { is NavTarget.JoinedRoom -> {
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>() val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
@ -205,10 +211,12 @@ class RoomFlowNode(
} }
is NavTarget.JoinedSpace -> { is NavTarget.JoinedSpace -> {
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single() val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
spaceEntryPoint.nodeBuilder(this, buildContext) spaceEntryPoint.createNode(
.inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId)) parentNode = this,
.callback(spaceCallback) buildContext = buildContext,
.build() inputs = SpaceEntryPoint.Inputs(roomId = navTarget.spaceId),
callback = spaceCallback,
)
} }
} }
} }

View file

@ -31,6 +31,7 @@ import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.DependencyInjectionGraphOwner
@ -76,9 +77,9 @@ class JoinedRoomLoadedFlowNode(
plugins = plugins, plugins = plugins,
), DependencyInjectionGraphOwner { ), DependencyInjectionGraphOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId, serverNames: List<String>) fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun onOpenGlobalNotificationSettings() fun navigateToGlobalNotificationSettings()
} }
data class Inputs( data class Inputs(
@ -87,7 +88,7 @@ class JoinedRoomLoadedFlowNode(
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val callbacks = plugins.filterIsInstance<Callback>() private val callback: Callback = callback()
override val graph = roomGraphFactory.create(inputs.room) override val graph = roomGraphFactory.create(inputs.room)
init { init {
@ -123,26 +124,28 @@ class JoinedRoomLoadedFlowNode(
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node { private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {
val callback = object : RoomDetailsEntryPoint.Callback { val callback = object : RoomDetailsEntryPoint.Callback {
override fun onOpenGlobalNotificationSettings() { override fun navigateToGlobalNotificationSettings() {
callbacks.forEach { it.onOpenGlobalNotificationSettings() } callback.navigateToGlobalNotificationSettings()
} }
override fun onOpenRoom(roomId: RoomId, serverNames: List<String>) { override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
callbacks.forEach { it.onOpenRoom(roomId, serverNames) } callback.navigateToRoom(roomId, serverNames)
} }
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } callback.handlePermalinkClick(data, pushToBackstack)
} }
override fun forwardEvent(eventId: EventId) { override fun startForwardEventFlow(eventId: EventId) {
backstack.push(NavTarget.ForwardEvent(eventId)) backstack.push(NavTarget.ForwardEvent(eventId))
} }
} }
return roomDetailsEntryPoint.nodeBuilder(this, buildContext) return roomDetailsEntryPoint.createNode(
.params(RoomDetailsEntryPoint.Params(initialTarget)) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = RoomDetailsEntryPoint.Params(initialTarget),
callback = callback,
)
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -172,36 +175,40 @@ class JoinedRoomLoadedFlowNode(
override fun onDone(roomIds: List<RoomId>) { override fun onDone(roomIds: List<RoomId>) {
backstack.pop() backstack.pop()
roomIds.singleOrNull()?.let { roomId -> roomIds.singleOrNull()?.let { roomId ->
callbacks.forEach { it.onOpenRoom(roomId, emptyList()) } callback.navigateToRoom(roomId, emptyList())
} }
} }
} }
forwardEntryPoint.nodeBuilder(this, buildContext) forwardEntryPoint.createNode(
.params(params) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = params,
callback = callback,
)
} }
} }
} }
private fun createSpaceNode(buildContext: BuildContext): Node { private fun createSpaceNode(buildContext: BuildContext): Node {
val callback = object : SpaceEntryPoint.Callback { val callback = object : SpaceEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) { override fun navigateToRoom(roomId: RoomId, viaParameters: List<String>) {
callbacks.forEach { it.onOpenRoom(roomId, viaParameters) } callback.navigateToRoom(roomId, viaParameters)
} }
override fun onOpenDetails() { override fun navigateToRoomDetails() {
backstack.push(NavTarget.RoomDetails) backstack.push(NavTarget.RoomDetails)
} }
override fun onOpenMemberList() { override fun navigateToRoomMemberList() {
backstack.push(NavTarget.RoomMemberList) backstack.push(NavTarget.RoomMemberList)
} }
} }
return spaceEntryPoint.nodeBuilder(this, buildContext) return spaceEntryPoint.createNode(
.inputs(SpaceEntryPoint.Inputs(roomId = inputs.room.roomId)) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() inputs = SpaceEntryPoint.Inputs(roomId = inputs.room.roomId),
callback = callback,
)
} }
private fun createMessagesNode( private fun createMessagesNode(
@ -209,33 +216,35 @@ class JoinedRoomLoadedFlowNode(
navTarget: NavTarget.Messages, navTarget: NavTarget.Messages,
): Node { ): Node {
val callback = object : MessagesEntryPoint.Callback { val callback = object : MessagesEntryPoint.Callback {
override fun onRoomDetailsClick() { override fun navigateToRoomDetails() {
backstack.push(NavTarget.RoomDetails) backstack.push(NavTarget.RoomDetails)
} }
override fun onUserDataClick(userId: UserId) { override fun navigateToRoomMemberDetails(userId: UserId) {
backstack.push(NavTarget.RoomMemberDetails(userId)) backstack.push(NavTarget.RoomMemberDetails(userId))
} }
override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) {
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } callback.handlePermalinkClick(data, pushToBackstack)
} }
override fun forwardEvent(eventId: EventId) { override fun forwardEvent(eventId: EventId) {
backstack.push(NavTarget.ForwardEvent(eventId)) backstack.push(NavTarget.ForwardEvent(eventId))
} }
override fun openRoom(roomId: RoomId) { override fun navigateToRoom(roomId: RoomId) {
callbacks.forEach { it.onOpenRoom(roomId, emptyList()) } callback.navigateToRoom(roomId, emptyList())
} }
} }
val params = MessagesEntryPoint.Params( val params = MessagesEntryPoint.Params(
MessagesEntryPoint.InitialTarget.Messages(navTarget.focusedEventId) MessagesEntryPoint.InitialTarget.Messages(navTarget.focusedEventId)
) )
return messagesEntryPoint.nodeBuilder(this, buildContext) return messagesEntryPoint.createNode(
.params(params) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = params,
callback = callback,
)
} }
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {

View file

@ -19,8 +19,10 @@ import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.di.RoomGraphFactory import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.FakeJoinedRoomLoadedFlowNodeCallback
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.features.forward.test.FakeForwardEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.space.api.SpaceEntryPoint
@ -48,29 +50,20 @@ class JoinedRoomLoadedFlowNodeTest {
@get:Rule @get:Rule
val mainDispatcherRule = MainDispatcherRule() val mainDispatcherRule = MainDispatcherRule()
private class FakeMessagesEntryPoint : MessagesEntryPoint, MessagesEntryPoint.NodeBuilder { private class FakeMessagesEntryPoint : MessagesEntryPoint {
var buildContext: BuildContext? = null
var nodeId: String? = null var nodeId: String? = null
var parameters: MessagesEntryPoint.Params? = null var parameters: MessagesEntryPoint.Params? = null
var callback: MessagesEntryPoint.Callback? = null var callback: MessagesEntryPoint.Callback? = null
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder { override fun createNode(
this.buildContext = buildContext parentNode: Node,
return this buildContext: BuildContext,
} params: MessagesEntryPoint.Params,
callback: MessagesEntryPoint.Callback,
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder { ): Node {
parameters = params parameters = params
return this
}
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
this.callback = callback this.callback = callback
return this return node(buildContext) {}.also {
}
override fun build(): Node {
return node(buildContext!!) {}.also {
nodeId = it.id nodeId = it.id
} }
} }
@ -85,54 +78,26 @@ class JoinedRoomLoadedFlowNodeTest {
private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint { private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint {
var nodeId: String? = null var nodeId: String? = null
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { override fun createNode(
return object : RoomDetailsEntryPoint.NodeBuilder { parentNode: Node,
override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder { buildContext: BuildContext,
return this params: RoomDetailsEntryPoint.Params,
} callback: RoomDetailsEntryPoint.Callback,
) = node(buildContext) {}.also {
override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder { nodeId = it.id
return this
}
override fun build(): Node {
return node(buildContext) {}.also {
nodeId = it.id
}
}
}
} }
} }
private class FakeSpaceEntryPoint : SpaceEntryPoint { private class FakeSpaceEntryPoint : SpaceEntryPoint {
var nodeId: String? = null var nodeId: String? = null
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SpaceEntryPoint.NodeBuilder { override fun createNode(
return object : SpaceEntryPoint.NodeBuilder { parentNode: Node,
override fun inputs(inputs: SpaceEntryPoint.Inputs): SpaceEntryPoint.NodeBuilder { buildContext: BuildContext,
return this inputs: SpaceEntryPoint.Inputs,
} callback: SpaceEntryPoint.Callback,
) = node(buildContext) {}.also {
override fun callback(callback: SpaceEntryPoint.Callback): SpaceEntryPoint.NodeBuilder { nodeId = it.id
return this
}
override fun build(): Node {
return node(buildContext) {}.also {
nodeId = it.id
}
}
}
}
}
private class FakeForwardEntryPoint : ForwardEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ForwardEntryPoint.NodeBuilder {
return object : ForwardEntryPoint.NodeBuilder {
override fun params(params: ForwardEntryPoint.Params) = this
override fun callback(callback: ForwardEntryPoint.Callback) = this
override fun build() = node(buildContext) {}
}
} }
} }
@ -165,7 +130,7 @@ class JoinedRoomLoadedFlowNodeTest {
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
) )
// WHEN // WHEN
@ -185,7 +150,7 @@ class JoinedRoomLoadedFlowNodeTest {
val spaceEntryPoint = FakeSpaceEntryPoint() val spaceEntryPoint = FakeSpaceEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
spaceEntryPoint = spaceEntryPoint, spaceEntryPoint = spaceEntryPoint,
) )
// WHEN // WHEN
@ -206,13 +171,13 @@ class JoinedRoomLoadedFlowNodeTest {
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
) )
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// WHEN // WHEN
fakeMessagesEntryPoint.callback?.onRoomDetailsClick() fakeMessagesEntryPoint.callback?.navigateToRoomDetails()
// THEN // THEN
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!! val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
@ -228,7 +193,7 @@ class JoinedRoomLoadedFlowNodeTest {
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val activeRoomsHolder = ActiveRoomsHolder() val activeRoomsHolder = ActiveRoomsHolder()
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,
@ -253,7 +218,7 @@ class JoinedRoomLoadedFlowNodeTest {
addRoom(room) addRoom(room)
} }
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,

View file

@ -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.appnav.room.joined
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.tests.testutils.lambda.lambdaError
class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
override fun navigateToGlobalNotificationSettings() = lambdaError()
}

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.features.changeroommemberroes.api package io.element.android.features.changeroommemberroles.api
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -15,13 +15,12 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint { fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
fun builder(parentNode: Node, buildContext: BuildContext): Builder fun createNode(
parentNode: Node,
interface Builder { buildContext: BuildContext,
fun room(room: JoinedRoom): Builder room: JoinedRoom,
fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): Builder listType: ChangeRoomMemberRolesListType,
fun build(): Node ): Node
}
interface NodeProxy { interface NodeProxy {
val roomId: RoomId val roomId: RoomId

View file

@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs

View file

@ -20,8 +20,8 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.RoomGraphFactory import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs

View file

@ -10,37 +10,25 @@ package io.element.android.features.changeroommemberroles.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultChangeRoomMemberRolesEntyPoint : ChangeRoomMemberRolesEntryPoint { class DefaultChangeRoomMemberRolesEntyPoint : ChangeRoomMemberRolesEntryPoint {
override fun builder(parentNode: Node, buildContext: BuildContext): ChangeRoomMemberRolesEntryPoint.Builder { override fun createNode(
return object : ChangeRoomMemberRolesEntryPoint.Builder { parentNode: Node,
private lateinit var changeRoomMemberRolesListType: ChangeRoomMemberRolesListType buildContext: BuildContext,
private lateinit var room: JoinedRoom room: JoinedRoom,
listType: ChangeRoomMemberRolesListType,
override fun room(room: JoinedRoom): ChangeRoomMemberRolesEntryPoint.Builder { ): Node {
this.room = room return parentNode.createNode<ChangeRoomMemberRolesRootNode>(
return this buildContext = buildContext,
} plugins = listOf(
ChangeRoomMemberRolesRootNode.Inputs(joinedRoom = room, listType = listType),
override fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): ChangeRoomMemberRolesEntryPoint.Builder { )
this.changeRoomMemberRolesListType = changeRoomMemberRolesListType )
return this
}
override fun build(): Node {
return parentNode.createNode<ChangeRoomMemberRolesRootNode>(
buildContext = buildContext,
plugins = listOf(
ChangeRoomMemberRolesRootNode.Inputs(joinedRoom = room, listType = changeRoomMemberRolesListType),
)
)
}
}
} }
} }

View file

@ -8,7 +8,7 @@
package io.element.android.features.changeroommemberroles.impl package io.element.android.features.changeroommemberroles.impl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import org.junit.Test import org.junit.Test

View file

@ -10,7 +10,7 @@ package io.element.android.features.changeroommemberroles.impl
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -31,10 +31,12 @@ class DefaultChangeRoomMemberRolesEntyPointTest {
} }
val room = FakeJoinedRoom() val room = FakeJoinedRoom()
val listType = ChangeRoomMemberRolesListType.Admins val listType = ChangeRoomMemberRolesListType.Admins
val result = entryPoint.builder(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(
.room(FakeJoinedRoom()) parentNode = parentNode,
.listType(listType) buildContext = BuildContext.root(null),
.build() room = FakeJoinedRoom(),
listType = listType,
)
assertThat(result).isInstanceOf(ChangeRoomMemberRolesRootNode::class.java) assertThat(result).isInstanceOf(ChangeRoomMemberRolesRootNode::class.java)
// Search for the Inputs plugin // Search for the Inputs plugin
val input = result.plugins.filterIsInstance<ChangeRoomMemberRolesRootNode.Inputs>().single() val input = result.plugins.filterIsInstance<ChangeRoomMemberRolesRootNode.Inputs>().single()

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.changeroommemberroles.test"
}
dependencies {
implementation(projects.features.changeroommemberroles.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,26 @@
/*
* 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.changeroommemberroles.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.tests.testutils.lambda.lambdaError
class FakeChangeRoomMemberRolesEntryPoint : ChangeRoomMemberRolesEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
room: JoinedRoom,
listType: ChangeRoomMemberRolesListType,
): Node {
lambdaError()
}
}

View file

@ -14,12 +14,11 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint { interface CreateRoomEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun createNode(
parentNode: Node,
interface NodeBuilder { buildContext: BuildContext,
fun callback(callback: Callback): NodeBuilder callback: Callback,
fun build(): Node ): Node
}
interface Callback : Plugin { interface Callback : Plugin {
fun onRoomCreated(roomId: RoomId) fun onRoomCreated(roomId: RoomId)

View file

@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
@ -24,6 +23,7 @@ import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -42,9 +42,7 @@ class CreateRoomFlowNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
private fun onRoomCreated(roomId: RoomId) { private val callback: CreateRoomEntryPoint.Callback = callback()
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onRoomCreated(roomId) }
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
@ -60,7 +58,7 @@ class CreateRoomFlowNode(
val inputs = AddPeopleNode.Inputs(navTarget.roomId) val inputs = AddPeopleNode.Inputs(navTarget.roomId)
val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback { val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback {
override fun onFinish() { override fun onFinish() {
onRoomCreated(navTarget.roomId) callback.onRoomCreated(navTarget.roomId)
} }
} }
createNode<AddPeopleNode>(buildContext, plugins = listOf(inputs, callback)) createNode<AddPeopleNode>(buildContext, plugins = listOf(inputs, callback))

View file

@ -9,7 +9,6 @@ package io.element.android.features.createroom.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -17,18 +16,11 @@ import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint { class DefaultCreateRoomEntryPoint : CreateRoomEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder { override fun createNode(
val plugins = ArrayList<Plugin>() parentNode: Node,
buildContext: BuildContext,
return object : CreateRoomEntryPoint.NodeBuilder { callback: CreateRoomEntryPoint.Callback,
override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder { ): Node {
plugins += callback return parentNode.createNode<CreateRoomFlowNode>(buildContext, listOf(callback))
return this
}
override fun build(): Node {
return parentNode.createNode<CreateRoomFlowNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -12,13 +12,13 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleRenderer import io.element.android.features.invitepeople.api.InvitePeopleRenderer
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -39,10 +39,7 @@ class AddPeopleNode(
fun onFinish() fun onFinish()
} }
private fun onFinish() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onFinish() }
}
private val roomId = inputs<Inputs>().roomId private val roomId = inputs<Inputs>().roomId
private val invitePeoplePresenter = invitePeoplePresenterFactory.create( private val invitePeoplePresenter = invitePeoplePresenterFactory.create(
joinedRoom = null, joinedRoom = null,
@ -54,7 +51,7 @@ class AddPeopleNode(
val state = invitePeoplePresenter.present() val state = invitePeoplePresenter.present()
AddPeopleView( AddPeopleView(
state = state, state = state,
onFinish = ::onFinish, onFinish = callback::onFinish,
) { ) {
invitePeopleRenderer.Render(state, Modifier) invitePeopleRenderer.Render(state, Modifier)
} }

View file

@ -13,11 +13,11 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -42,9 +42,7 @@ class ConfigureRoomNode(
) )
} }
private fun onCreateRoomSuccess(roomId: RoomId) { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -53,7 +51,7 @@ class ConfigureRoomNode(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackClick = this::navigateUp, onBackClick = this::navigateUp,
onCreateRoomSuccess = ::onCreateRoomSuccess, onCreateRoomSuccess = callback::onCreateRoomSuccess,
) )
} }
} }

View file

@ -38,9 +38,11 @@ class DefaultCreateRoomEntryPointTest {
val callback = object : CreateRoomEntryPoint.Callback { val callback = object : CreateRoomEntryPoint.Callback {
override fun onRoomCreated(roomId: RoomId) = lambdaError() override fun onRoomCreated(roomId: RoomId) = lambdaError()
} }
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(
.callback(callback) parentNode = parentNode,
.build() buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)
} }
} }

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.createroom.test"
}
dependencies {
implementation(projects.features.createroom.api)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,20 @@
/*
* 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.createroom.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.tests.testutils.lambda.lambdaError
class FakeCreateRoomEntryPoint : CreateRoomEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: CreateRoomEntryPoint.Callback,
): Node = lambdaError()
}

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.deactivation.test"
}
dependencies {
implementation(projects.features.deactivation.api)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,22 @@
/*
* 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.deactivation.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeAccountDeactivationEntryPoint : AccountDeactivationEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
): Node {
lambdaError()
}
}

View file

@ -17,12 +17,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.TimelineProvider
interface ForwardEntryPoint : FeatureEntryPoint { interface ForwardEntryPoint : FeatureEntryPoint {
interface NodeBuilder {
fun params(params: Params): NodeBuilder
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin { interface Callback : Plugin {
fun onDone(roomIds: List<RoomId>) fun onDone(roomIds: List<RoomId>)
} }
@ -32,5 +26,10 @@ interface ForwardEntryPoint : FeatureEntryPoint {
val timelineProvider: TimelineProvider, val timelineProvider: TimelineProvider,
) : NodeInputs ) : NodeInputs
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: Params,
callback: Callback,
): Node
} }

View file

@ -34,5 +34,6 @@ dependencies {
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.roomselect.test)
testImplementation(projects.libraries.testtags) testImplementation(projects.libraries.testtags)
} }

View file

@ -9,7 +9,6 @@ package io.element.android.features.forward.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -17,26 +16,21 @@ import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultForwardEntryPoint : ForwardEntryPoint { class DefaultForwardEntryPoint : ForwardEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ForwardEntryPoint.NodeBuilder { override fun createNode(
val plugins = ArrayList<Plugin>() parentNode: Node,
buildContext: BuildContext,
return object : ForwardEntryPoint.NodeBuilder { params: ForwardEntryPoint.Params,
override fun params(params: ForwardEntryPoint.Params): ForwardEntryPoint.NodeBuilder { callback: ForwardEntryPoint.Callback,
plugins += ForwardMessagesNode.Inputs( ): Node {
return parentNode.createNode<ForwardMessagesNode>(
buildContext = buildContext,
plugins = listOf(
ForwardMessagesNode.Inputs(
eventId = params.eventId, eventId = params.eventId,
timelineProvider = params.timelineProvider, timelineProvider = params.timelineProvider,
) ),
return this callback,
} )
)
override fun callback(callback: ForwardEntryPoint.Callback): ForwardEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<ForwardMessagesNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -22,6 +22,7 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -55,8 +56,8 @@ class ForwardMessagesNode(
) : NodeInputs ) : NodeInputs
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val callback: ForwardEntryPoint.Callback = callback()
private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider) private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider)
private val callbacks = plugins.filterIsInstance<ForwardEntryPoint.Callback>()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
val callback = object : RoomSelectEntryPoint.Callback { val callback = object : RoomSelectEntryPoint.Callback {
@ -65,14 +66,16 @@ class ForwardMessagesNode(
} }
override fun onCancel() { override fun onCancel() {
onForwardDone(emptyList()) callback.onDone(emptyList())
} }
} }
return roomSelectEntryPoint.nodeBuilder(this, buildContext) return roomSelectEntryPoint.createNode(
.callback(callback) parentNode = this,
.params(RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward)) buildContext = buildContext,
.build() params = RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward),
callback = callback,
)
} }
@Composable @Composable
@ -86,12 +89,8 @@ class ForwardMessagesNode(
val state = presenter.present() val state = presenter.present()
ForwardMessagesView( ForwardMessagesView(
state = state, state = state,
onForwardSuccess = ::onForwardDone, onForwardSuccess = callback::onDone,
) )
} }
} }
private fun onForwardDone(roomIds: List<RoomId>) {
callbacks.forEach { it.onDone(roomIds) }
}
} }

View file

@ -9,14 +9,13 @@ package io.element.android.features.forward.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.test.FakeRoomSelectEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -38,11 +37,7 @@ class DefaultForwardEntryPointTest {
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
presenterFactory = { _, _ -> createForwardMessagesPresenter() }, presenterFactory = { _, _ -> createForwardMessagesPresenter() },
roomSelectEntryPoint = object : RoomSelectEntryPoint { roomSelectEntryPoint = FakeRoomSelectEntryPoint(),
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder {
lambdaError()
}
}
) )
} }
val callback = object : ForwardEntryPoint.Callback { val callback = object : ForwardEntryPoint.Callback {
@ -52,10 +47,12 @@ class DefaultForwardEntryPointTest {
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
timelineProvider = FakeTimelineProvider(), timelineProvider = FakeTimelineProvider(),
) )
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(
.params(params) parentNode = parentNode,
.callback(callback) buildContext = BuildContext.root(null),
.build() params = params,
callback = callback,
)
assertThat(result).isInstanceOf(ForwardMessagesNode::class.java) assertThat(result).isInstanceOf(ForwardMessagesNode::class.java)
assertThat(result.plugins).contains( assertThat(result.plugins).contains(
ForwardMessagesNode.Inputs( ForwardMessagesNode.Inputs(

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.forward.test"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.features.forward.api)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,22 @@
/*
* 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.forward.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeForwardEntryPoint : ForwardEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: ForwardEntryPoint.Params,
callback: ForwardEntryPoint.Callback,
): Node = lambdaError()
}

View file

@ -110,9 +110,12 @@ class FtueFlowNode(
defaultFtueService.updateFtueStep() defaultFtueService.updateFtueStep()
} }
} }
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup) lockScreenEntryPoint.createNode(
.callback(callback) parentNode = this,
.build() buildContext = buildContext,
navTarget = LockScreenEntryPoint.Target.Setup,
callback = callback,
)
} }
} }
} }

View file

@ -15,7 +15,6 @@ import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
@ -29,6 +28,7 @@ import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.utils.OpenUrlInTabView import io.element.android.libraries.designsystem.utils.OpenUrlInTabView
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -69,6 +69,8 @@ class FtueSessionVerificationFlowNode(
fun onDone() fun onDone()
} }
private val callback: Callback = callback()
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback { private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
override fun onDone() { override fun onDone() {
lifecycleScope.launch { lifecycleScope.launch {
@ -82,62 +84,67 @@ class FtueSessionVerificationFlowNode(
return when (navTarget) { return when (navTarget) {
is NavTarget.Root -> { is NavTarget.Root -> {
val callback = object : ChooseSelfVerificationModeNode.Callback { val callback = object : ChooseSelfVerificationModeNode.Callback {
override fun onUseAnotherDevice() { override fun navigateToUseAnotherDevice() {
backstack.push(NavTarget.UseAnotherDevice) backstack.push(NavTarget.UseAnotherDevice)
} }
override fun onUseRecoveryKey() { override fun navigateToUseRecoveryKey() {
backstack.push(NavTarget.EnterRecoveryKey) backstack.push(NavTarget.EnterRecoveryKey)
} }
override fun onResetKey() { override fun navigateToResetKey() {
backstack.push(NavTarget.ResetIdentity) backstack.push(NavTarget.ResetIdentity)
} }
override fun onLearnMoreAboutEncryption() { override fun navigateToLearnMoreAboutEncryption() {
learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL
} }
} }
createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback)) createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback))
} }
is NavTarget.UseAnotherDevice -> { is NavTarget.UseAnotherDevice -> {
outgoingVerificationEntryPoint.nodeBuilder(this, buildContext) outgoingVerificationEntryPoint.createNode(
.params(OutgoingVerificationEntryPoint.Params( parentNode = this,
buildContext = buildContext,
params = OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = true, showDeviceVerifiedScreen = true,
verificationRequest = VerificationRequest.Outgoing.CurrentSession, verificationRequest = VerificationRequest.Outgoing.CurrentSession,
)) ),
.callback(object : OutgoingVerificationEntryPoint.Callback { callback = object : OutgoingVerificationEntryPoint.Callback {
override fun onDone() { override fun onDone() {
plugins<Callback>().forEach { it.onDone() } callback.onDone()
} }
override fun onBack() { override fun onBack() {
backstack.pop() backstack.pop()
} }
override fun onLearnMoreAboutEncryption() { override fun navigateToLearnMoreAboutEncryption() {
// Note that this callback is never called. The "Learn more" link is not displayed // Note that this callback is never called. The "Learn more" link is not displayed
// for the self session interactive verification. // for the self session interactive verification.
} }
}) }
.build() )
} }
is NavTarget.EnterRecoveryKey -> { is NavTarget.EnterRecoveryKey -> {
secureBackupEntryPoint.nodeBuilder(this, buildContext) secureBackupEntryPoint.createNode(
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) parentNode = this,
.callback(secureBackupEntryPointCallback) buildContext = buildContext,
.build() params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey),
callback = secureBackupEntryPointCallback
)
} }
is NavTarget.ResetIdentity -> { is NavTarget.ResetIdentity -> {
secureBackupEntryPoint.nodeBuilder(this, buildContext) secureBackupEntryPoint.createNode(
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity)) parentNode = this,
.callback(object : SecureBackupEntryPoint.Callback { buildContext = buildContext,
params = SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity),
callback = object : SecureBackupEntryPoint.Callback {
override fun onDone() { override fun onDone() {
plugins<Callback>().forEach { it.onDone() } callback.onDone()
} }
}) },
.build() )
} }
} }
} }

View file

@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -29,13 +29,13 @@ class ChooseSelfVerificationModeNode(
private val directLogoutView: DirectLogoutView, private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onUseAnotherDevice() fun navigateToUseAnotherDevice()
fun onUseRecoveryKey() fun navigateToUseRecoveryKey()
fun onResetKey() fun navigateToResetKey()
fun onLearnMoreAboutEncryption() fun navigateToLearnMoreAboutEncryption()
} }
private val callback = plugins<Callback>().first() private val callback: Callback = callback()
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -43,10 +43,10 @@ class ChooseSelfVerificationModeNode(
ChooseSelfVerificationModeView( ChooseSelfVerificationModeView(
state = state, state = state,
onUseAnotherDevice = callback::onUseAnotherDevice, onUseAnotherDevice = callback::navigateToUseAnotherDevice,
onUseRecoveryKey = callback::onUseRecoveryKey, onUseRecoveryKey = callback::navigateToUseRecoveryKey,
onResetKey = callback::onResetKey, onResetKey = callback::navigateToResetKey,
onLearnMore = callback::onLearnMoreAboutEncryption, onLearnMore = callback::navigateToLearnMoreAboutEncryption,
modifier = modifier, modifier = modifier,
) )

View file

@ -7,13 +7,11 @@
package io.element.android.features.ftue.impl package io.element.android.features.ftue.impl
import android.content.Context
import android.content.Intent
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.test.FakeLockScreenEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -36,19 +34,7 @@ class DefaultFtueEntryPointTest {
plugins = plugins, plugins = plugins,
analyticsEntryPoint = { _, _ -> lambdaError() }, analyticsEntryPoint = { _, _ -> lambdaError() },
defaultFtueService = createDefaultFtueService(), defaultFtueService = createDefaultFtueService(),
lockScreenEntryPoint = object : LockScreenEntryPoint { lockScreenEntryPoint = FakeLockScreenEntryPoint(),
override fun nodeBuilder(
parentNode: com.bumble.appyx.core.node.Node,
buildContext: BuildContext,
navTarget: LockScreenEntryPoint.Target
): LockScreenEntryPoint.NodeBuilder {
lambdaError()
}
override fun pinUnlockIntent(context: Context): Intent {
lambdaError()
}
},
) )
} }
val result = entryPoint.createNode(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(parentNode, BuildContext.root(null))

View file

@ -14,19 +14,19 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface HomeEntryPoint : FeatureEntryPoint { interface HomeEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun createNode(
interface NodeBuilder { parentNode: Node,
fun callback(callback: Callback): NodeBuilder buildContext: BuildContext,
fun build(): Node callback: Callback,
} ): Node
interface Callback : Plugin { interface Callback : Plugin {
fun onRoomClick(roomId: RoomId) fun navigateToRoom(roomId: RoomId)
fun onStartChatClick() fun navigateToCreateRoom()
fun onSettingsClick() fun navigateToSettings()
fun onSetUpRecoveryClick() fun navigateToSetUpRecovery()
fun onSessionConfirmRecoveryKeyClick() fun navigateToEnterRecoveryKey()
fun onRoomSettingsClick(roomId: RoomId) fun navigateToRoomSettings(roomId: RoomId)
fun onReportBugClick() fun navigateToBugReport()
} }
} }

View file

@ -9,7 +9,6 @@ package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.api.HomeEntryPoint
@ -17,18 +16,11 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultHomeEntryPoint : HomeEntryPoint { class DefaultHomeEntryPoint : HomeEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): HomeEntryPoint.NodeBuilder { override fun createNode(
val plugins = ArrayList<Plugin>() parentNode: Node,
buildContext: BuildContext,
return object : HomeEntryPoint.NodeBuilder { callback: HomeEntryPoint.Callback,
override fun callback(callback: HomeEntryPoint.Callback): HomeEntryPoint.NodeBuilder { ): Node {
plugins += callback return parentNode.createNode<HomeFlowNode>(buildContext, listOf(callback))
return this
}
override fun build(): Node {
return parentNode.createNode<HomeFlowNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -21,7 +21,6 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
@ -29,8 +28,8 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType import io.element.android.features.changeroommemberroles.api.ChangeRoomMemberRolesListType
import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.home.impl.components.RoomListMenuAction import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.model.RoomListRoomSummary
@ -44,6 +43,7 @@ import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -78,6 +78,7 @@ class HomeFlowNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
private val callback: HomeEntryPoint.Callback = callback()
private val stateFlow = launchMolecule { presenter.present() } private val stateFlow = launchMolecule { presenter.present() }
override fun onBuilt() { override fun onBuilt() {
@ -115,35 +116,11 @@ class HomeFlowNode(
data class SelectNewOwnersWhenLeavingRoom(val roomId: RoomId) : NavTarget data class SelectNewOwnersWhenLeavingRoom(val roomId: RoomId) : NavTarget
} }
private fun onRoomClick(roomId: RoomId) { private fun navigateToReportRoom(roomId: RoomId) {
plugins<HomeEntryPoint.Callback>().forEach { it.onRoomClick(roomId) }
}
private fun onOpenSettings() {
plugins<HomeEntryPoint.Callback>().forEach { it.onSettingsClick() }
}
private fun onStartChatClick() {
plugins<HomeEntryPoint.Callback>().forEach { it.onStartChatClick() }
}
private fun onSetUpRecoveryClick() {
plugins<HomeEntryPoint.Callback>().forEach { it.onSetUpRecoveryClick() }
}
private fun onSessionConfirmRecoveryKeyClick() {
plugins<HomeEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClick() }
}
private fun onRoomSettingsClick(roomId: RoomId) {
plugins<HomeEntryPoint.Callback>().forEach { it.onRoomSettingsClick(roomId) }
}
private fun onReportRoomClick(roomId: RoomId) {
backstack.push(NavTarget.ReportRoom(roomId)) backstack.push(NavTarget.ReportRoom(roomId))
} }
private fun onDeclineInviteAndBlockUserClick(roomSummary: RoomListRoomSummary) { private fun navigateToDeclineInviteAndBlockUser(roomSummary: RoomListRoomSummary) {
backstack.push(NavTarget.DeclineInviteAndBlockUser(roomSummary.toInviteData())) backstack.push(NavTarget.DeclineInviteAndBlockUser(roomSummary.toInviteData()))
} }
@ -153,12 +130,12 @@ class HomeFlowNode(
inviteFriendsUseCase.execute(activity) inviteFriendsUseCase.execute(activity)
} }
RoomListMenuAction.ReportBug -> { RoomListMenuAction.ReportBug -> {
plugins<HomeEntryPoint.Callback>().forEach { it.onReportBugClick() } callback.navigateToBugReport()
} }
} }
} }
private fun onSelectNewOwnersWhenLeavingRoom(roomId: RoomId) { private fun navigateToSelectNewOwnersWhenLeavingRoom(roomId: RoomId) {
backstack.push(NavTarget.SelectNewOwnersWhenLeavingRoom(roomId)) backstack.push(NavTarget.SelectNewOwnersWhenLeavingRoom(roomId))
} }
@ -172,20 +149,20 @@ class HomeFlowNode(
val activity = requireNotNull(LocalActivity.current) val activity = requireNotNull(LocalActivity.current)
HomeView( HomeView(
homeState = state, homeState = state,
onRoomClick = this::onRoomClick, onRoomClick = callback::navigateToRoom,
onSettingsClick = this::onOpenSettings, onSettingsClick = callback::navigateToSettings,
onStartChatClick = this::onStartChatClick, onStartChatClick = callback::navigateToCreateRoom,
onSetUpRecoveryClick = this::onSetUpRecoveryClick, onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = callback::navigateToEnterRecoveryKey,
onRoomSettingsClick = this::onRoomSettingsClick, onRoomSettingsClick = callback::navigateToRoomSettings,
onMenuActionClick = { onMenuActionClick(activity, it) }, onMenuActionClick = { onMenuActionClick(activity, it) },
onReportRoomClick = this::onReportRoomClick, onReportRoomClick = ::navigateToReportRoom,
onDeclineInviteAndBlockUser = this::onDeclineInviteAndBlockUserClick, onDeclineInviteAndBlockUser = ::navigateToDeclineInviteAndBlockUser,
modifier = modifier, modifier = modifier,
acceptDeclineInviteView = { acceptDeclineInviteView = {
acceptDeclineInviteView.Render( acceptDeclineInviteView.Render(
state = state.roomListState.acceptDeclineInviteState, state = state.roomListState.acceptDeclineInviteState,
onAcceptInviteSuccess = this::onRoomClick, onAcceptInviteSuccess = callback::navigateToRoom,
onDeclineInviteSuccess = { }, onDeclineInviteSuccess = { },
modifier = Modifier modifier = Modifier
) )
@ -193,7 +170,7 @@ class HomeFlowNode(
leaveRoomView = { leaveRoomView = {
leaveRoomRenderer.Render( leaveRoomRenderer.Render(
state = state.roomListState.leaveRoomState, state = state.roomListState.leaveRoomState,
onSelectNewOwners = this::onSelectNewOwnersWhenLeavingRoom, onSelectNewOwners = ::navigateToSelectNewOwnersWhenLeavingRoom,
modifier = Modifier modifier = Modifier
) )
} }
@ -209,14 +186,28 @@ class HomeFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.ReportRoom -> reportRoomEntryPoint.createNode(this, buildContext, navTarget.roomId) is NavTarget.ReportRoom -> {
is NavTarget.DeclineInviteAndBlockUser -> declineInviteAndBlockUserEntryPoint.createNode(this, buildContext, navTarget.inviteData) reportRoomEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
roomId = navTarget.roomId,
)
}
is NavTarget.DeclineInviteAndBlockUser -> {
declineInviteAndBlockUserEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
)
}
is NavTarget.SelectNewOwnersWhenLeavingRoom -> { is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
val room = runBlocking { matrixClient.getJoinedRoom(navTarget.roomId) } ?: error("Room ${navTarget.roomId} not found") val room = runBlocking { matrixClient.getJoinedRoom(navTarget.roomId) } ?: error("Room ${navTarget.roomId} not found")
changeRoomMemberRolesEntryPoint.builder(this, buildContext) changeRoomMemberRolesEntryPoint.createNode(
.room(room) parentNode = this,
.listType(ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving) buildContext = buildContext,
.build() room = room,
listType = ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving,
)
} }
NavTarget.Root -> rootNode(buildContext) NavTarget.Root -> rootNode(buildContext)
} }

View file

@ -36,22 +36,24 @@ class DefaultHomeEntryPointTest {
directLogoutView = { _ -> lambdaError() }, directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() }, reportRoomEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() }, declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
changeRoomMemberRolesEntryPoint = { _, _ -> lambdaError() }, changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
leaveRoomRenderer = { _, _, _ -> lambdaError() }, leaveRoomRenderer = { _, _, _ -> lambdaError() },
) )
} }
val callback = object : HomeEntryPoint.Callback { val callback = object : HomeEntryPoint.Callback {
override fun onRoomClick(roomId: RoomId) = lambdaError() override fun navigateToRoom(roomId: RoomId) = lambdaError()
override fun onStartChatClick() = lambdaError() override fun navigateToCreateRoom() = lambdaError()
override fun onSettingsClick() = lambdaError() override fun navigateToSettings() = lambdaError()
override fun onSetUpRecoveryClick() = lambdaError() override fun navigateToSetUpRecovery() = lambdaError()
override fun onSessionConfirmRecoveryKeyClick() = lambdaError() override fun navigateToEnterRecoveryKey() = lambdaError()
override fun onRoomSettingsClick(roomId: RoomId) = lambdaError() override fun navigateToRoomSettings(roomId: RoomId) = lambdaError()
override fun onReportBugClick() = lambdaError() override fun navigateToBugReport() = lambdaError()
} }
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(
.callback(callback) parentNode = parentNode,
.build() buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result).isInstanceOf(HomeFlowNode::class.java) assertThat(result).isInstanceOf(HomeFlowNode::class.java)
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)
} }

View file

@ -13,5 +13,9 @@ import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint { fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node fun createNode(
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
): Node
} }

View file

@ -17,7 +17,11 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultDeclineAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint { class DefaultDeclineAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node { override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData) val inputs = DeclineAndBlockNode.Inputs(inviteData)
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs)) return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs))
} }

View file

@ -26,5 +26,6 @@ dependencies {
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.test) implementation(projects.libraries.matrix.test)
implementation(projects.tests.testutils)
api(projects.features.invite.api) api(projects.features.invite.api)
} }

View file

@ -0,0 +1,24 @@
/*
* 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.invite.test.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeDeclineInviteAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
): Node {
lambdaError()
}
}

View file

@ -18,7 +18,11 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import java.util.Optional import java.util.Optional
interface JoinRoomEntryPoint : FeatureEntryPoint { interface JoinRoomEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: Inputs,
): Node
data class Inputs( data class Inputs(
val roomId: RoomId, val roomId: RoomId,

View file

@ -16,7 +16,11 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultJoinRoomEntryPoint : JoinRoomEntryPoint { class DefaultJoinRoomEntryPoint : JoinRoomEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node { override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: JoinRoomEntryPoint.Inputs,
): Node {
return parentNode.createNode<JoinRoomFlowNode>( return parentNode.createNode<JoinRoomFlowNode>(
buildContext = buildContext, buildContext = buildContext,
plugins = listOf(inputs) plugins = listOf(inputs)

View file

@ -64,7 +64,11 @@ class JoinRoomFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(this, buildContext, navTarget.inviteData) is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
)
NavTarget.Root -> rootNode(buildContext) NavTarget.Root -> rootNode(buildContext)
} }
} }

View file

@ -9,12 +9,10 @@ package io.element.android.features.joinroom.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -40,9 +38,7 @@ class DefaultJoinRoomEntryPointTest {
plugins = plugins, plugins = plugins,
presenterFactory = { _, _, _, _, _ -> createJoinRoomPresenter() }, presenterFactory = { _, _, _, _, _ -> createJoinRoomPresenter() },
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() }, acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
declineAndBlockEntryPoint = object : DeclineInviteAndBlockEntryPoint { declineAndBlockEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData) = lambdaError()
}
) )
} }
val inputs = JoinRoomEntryPoint.Inputs( val inputs = JoinRoomEntryPoint.Inputs(
@ -52,7 +48,11 @@ class DefaultJoinRoomEntryPointTest {
serverNames = emptyList(), serverNames = emptyList(),
trigger = JoinedRoom.Trigger.RoomDirectory, trigger = JoinedRoom.Trigger.RoomDirectory,
) )
val result = entryPoint.createNode(parentNode, BuildContext.root(null), inputs) val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
inputs = inputs,
)
assertThat(result).isInstanceOf(JoinRoomFlowNode::class.java) assertThat(result).isInstanceOf(JoinRoomFlowNode::class.java)
assertThat(result.plugins).contains(inputs) assertThat(result.plugins).contains(inputs)
} }

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.knockrequests.test"
}
dependencies {
implementation(projects.features.knockrequests.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,20 @@
/*
* 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.knockrequests.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeKnockRequestsListEntryPoint : KnockRequestsListEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
): Node = lambdaError()
}

View file

@ -52,7 +52,7 @@ class DependenciesFlowNode(
return when (navTarget) { return when (navTarget) {
is NavTarget.LicensesList -> { is NavTarget.LicensesList -> {
val callback = object : DependencyLicensesListNode.Callback { val callback = object : DependencyLicensesListNode.Callback {
override fun onOpenLicense(license: DependencyLicenseItem) { override fun navigateToLicense(license: DependencyLicenseItem) {
backstack.push(NavTarget.LicenseDetails(license)) backstack.push(NavTarget.LicenseDetails(license))
} }
} }

View file

@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@ -30,13 +30,10 @@ class DependencyLicensesListNode(
plugins = plugins plugins = plugins
) { ) {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenLicense(license: DependencyLicenseItem) fun navigateToLicense(license: DependencyLicenseItem)
} }
private fun onOpenLicense(license: DependencyLicenseItem) { private val callback: Callback = callback()
plugins<Callback>()
.forEach { it.onOpenLicense(license) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -44,7 +41,7 @@ class DependencyLicensesListNode(
DependencyLicensesListView( DependencyLicensesListView(
state = state, state = state,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onOpenLicense = ::onOpenLicense, onOpenLicense = callback::navigateToLicense,
) )
} }
} }

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.licenses.test"
}
dependencies {
implementation(projects.features.licenses.api)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,22 @@
/*
* 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.licenses.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeOpenSourceLicensesEntryPoint : OpenSourceLicensesEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
): Node {
lambdaError()
}
}

View file

@ -18,8 +18,9 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
* Allows a user to share a location message within a room. * Allows a user to share a location message within a room.
*/ */
interface SendLocationEntryPoint : FeatureEntryPoint { interface SendLocationEntryPoint : FeatureEntryPoint {
fun builder(timelineMode: Timeline.Mode): Builder fun createNode(
interface Builder { parentNode: Node,
fun build(parentNode: Node, buildContext: BuildContext): Node buildContext: BuildContext,
} timelineMode: Timeline.Mode,
): Node
} }

View file

@ -13,7 +13,14 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
interface ShowLocationEntryPoint : FeatureEntryPoint { interface ShowLocationEntryPoint : FeatureEntryPoint {
data class Inputs(val location: Location, val description: String?) : NodeInputs data class Inputs(
val location: Location,
val description: String?,
) : NodeInputs
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: Inputs,
): Node
} }

View file

@ -17,16 +17,14 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultSendLocationEntryPoint : SendLocationEntryPoint { class DefaultSendLocationEntryPoint : SendLocationEntryPoint {
override fun builder(timelineMode: Timeline.Mode): SendLocationEntryPoint.Builder { override fun createNode(
return Builder(timelineMode) parentNode: Node,
} buildContext: BuildContext,
timelineMode: Timeline.Mode,
class Builder(private val timelineMode: Timeline.Mode) : SendLocationEntryPoint.Builder { ): Node {
override fun build(parentNode: Node, buildContext: BuildContext): Node { return parentNode.createNode<SendLocationNode>(
return parentNode.createNode<SendLocationNode>( buildContext = buildContext,
buildContext = buildContext, plugins = listOf(SendLocationNode.Inputs(timelineMode))
plugins = listOf(SendLocationNode.Inputs(timelineMode)) )
)
}
} }
} }

View file

@ -16,7 +16,11 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultShowLocationEntryPoint : ShowLocationEntryPoint { class DefaultShowLocationEntryPoint : ShowLocationEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: ShowLocationEntryPoint.Inputs): Node { override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: ShowLocationEntryPoint.Inputs,
): Node {
return parentNode.createNode<ShowLocationNode>(buildContext, listOf(inputs)) return parentNode.createNode<ShowLocationNode>(buildContext, listOf(inputs))
} }
} }

View file

@ -47,8 +47,11 @@ class DefaultSendLocationEntryPointTest {
) )
} }
val timelineMode = Timeline.Mode.Live val timelineMode = Timeline.Mode.Live
val result = entryPoint.builder(timelineMode) val result = entryPoint.createNode(
.build(parentNode, BuildContext.root(null)) parentNode = parentNode,
buildContext = BuildContext.root(null),
timelineMode = timelineMode,
)
assertThat(result).isInstanceOf(SendLocationNode::class.java) assertThat(result).isInstanceOf(SendLocationNode::class.java)
assertThat(result.plugins).contains(SendLocationNode.Inputs(timelineMode)) assertThat(result.plugins).contains(SendLocationNode.Inputs(timelineMode))
} }

View file

@ -48,8 +48,8 @@ class DefaultShowLocationEntryPointTest {
description = "My location", description = "My location",
) )
val result = entryPoint.createNode( val result = entryPoint.createNode(
parentNode, parentNode = parentNode,
BuildContext.root(null), buildContext = BuildContext.root(null),
inputs = inputs, inputs = inputs,
) )
assertThat(result).isInstanceOf(ShowLocationNode::class.java) assertThat(result).isInstanceOf(ShowLocationNode::class.java)

View file

@ -14,5 +14,8 @@ android {
} }
dependencies { dependencies {
implementation(projects.features.location.api) api(projects.features.location.api)
implementation(projects.libraries.matrix.api)
implementation(libs.appyx.core)
implementation(projects.tests.testutils)
} }

View file

@ -0,0 +1,22 @@
/*
* 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.location.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.tests.testutils.lambda.lambdaError
class FakeSendLocationEntryPoint : SendLocationEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
timelineMode: Timeline.Mode,
): Node = lambdaError()
}

View file

@ -0,0 +1,21 @@
/*
* 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.location.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeShowLocationEntryPoint : ShowLocationEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: ShowLocationEntryPoint.Inputs,
): Node = lambdaError()
}

View file

@ -15,13 +15,14 @@ import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
interface LockScreenEntryPoint : FeatureEntryPoint { interface LockScreenEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: Target): NodeBuilder fun createNode(
fun pinUnlockIntent(context: Context): Intent parentNode: Node,
buildContext: BuildContext,
navTarget: Target,
callback: Callback,
): Node
interface NodeBuilder { fun pinUnlockIntent(context: Context): Intent
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin { interface Callback : Plugin {
fun onSetupDone() fun onSetupDone()

View file

@ -19,26 +19,24 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultLockScreenEntryPoint : LockScreenEntryPoint { class DefaultLockScreenEntryPoint : LockScreenEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { override fun createNode(
val callbacks = mutableListOf<LockScreenEntryPoint.Callback>() parentNode: Node,
buildContext: BuildContext,
return object : LockScreenEntryPoint.NodeBuilder { navTarget: LockScreenEntryPoint.Target,
override fun callback(callback: LockScreenEntryPoint.Callback): LockScreenEntryPoint.NodeBuilder { callback: LockScreenEntryPoint.Callback,
callbacks += callback ): Node {
return this return parentNode.createNode<LockScreenFlowNode>(
} buildContext = buildContext,
plugins = listOf(
override fun build(): Node { LockScreenFlowNode.Inputs(
val inputs = LockScreenFlowNode.Inputs(
when (navTarget) { when (navTarget) {
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
} }
) ),
val plugins = listOf(inputs) + callbacks callback,
return parentNode.createNode<LockScreenFlowNode>(buildContext, plugins) )
} )
}
} }
override fun pinUnlockIntent(context: Context): Intent { override fun pinUnlockIntent(context: Context): Intent {

View file

@ -110,7 +110,7 @@ class LockScreenSettingsFlowNode(
} }
NavTarget.Settings -> { NavTarget.Settings -> {
val callback = object : LockScreenSettingsNode.Callback { val callback = object : LockScreenSettingsNode.Callback {
override fun onChangePinClick() { override fun navigateToSetupPin() {
backstack.push(NavTarget.SetupPin) backstack.push(NavTarget.SetupPin)
} }
} }

View file

@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -26,12 +26,10 @@ class LockScreenSettingsNode(
private val presenter: LockScreenSettingsPresenter, private val presenter: LockScreenSettingsPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onChangePinClick() fun navigateToSetupPin()
} }
private fun onChangePinClick() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onChangePinClick() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -39,7 +37,7 @@ class LockScreenSettingsNode(
LockScreenSettingsView( LockScreenSettingsView(
state = state, state = state,
onBackClick = this::navigateUp, onBackClick = this::navigateUp,
onChangePinClick = this::onChangePinClick, onChangePinClick = callback::navigateToSetupPin,
modifier = modifier, modifier = modifier,
) )
} }

View file

@ -14,7 +14,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
@ -27,6 +26,7 @@ import io.element.android.features.lockscreen.impl.setup.biometric.SetupBiometri
import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode import io.element.android.features.lockscreen.impl.setup.pin.SetupPinNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -50,9 +50,7 @@ class LockScreenSetupFlowNode(
fun onSetupDone() fun onSetupDone()
} }
private fun onSetupDone() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onSetupDone() }
}
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
@ -67,7 +65,7 @@ class LockScreenSetupFlowNode(
if (biometricAuthenticatorManager.hasAvailableAuthenticator) { if (biometricAuthenticatorManager.hasAvailableAuthenticator) {
backstack.newRoot(NavTarget.Biometric) backstack.newRoot(NavTarget.Biometric)
} else { } else {
onSetupDone() callback.onSetupDone()
} }
} }
} }
@ -91,7 +89,7 @@ class LockScreenSetupFlowNode(
NavTarget.Biometric -> { NavTarget.Biometric -> {
val callback = object : SetupBiometricNode.Callback { val callback = object : SetupBiometricNode.Callback {
override fun onBiometricSetupDone() { override fun onBiometricSetupDone() {
onSetupDone() callback.onSetupDone()
} }
} }
createNode<SetupBiometricNode>(buildContext, plugins = listOf(callback)) createNode<SetupBiometricNode>(buildContext, plugins = listOf(callback))

View file

@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -30,16 +30,14 @@ class SetupBiometricNode(
fun onBiometricSetupDone() fun onBiometricSetupDone()
} }
private fun onSetupDone() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onBiometricSetupDone() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
LaunchedEffect(state.isBiometricSetupDone) { LaunchedEffect(state.isBiometricSetupDone) {
if (state.isBiometricSetupDone) { if (state.isBiometricSetupDone) {
onSetupDone() callback.onBiometricSetupDone()
} }
} }
SetupBiometricView( SetupBiometricView(

View file

@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -30,18 +30,14 @@ class PinUnlockNode(
fun onUnlock() fun onUnlock()
} }
private fun onUnlock() { private val callback: Callback = callback()
plugins<Callback>().forEach {
it.onUnlock()
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
LaunchedEffect(state.isUnlocked) { LaunchedEffect(state.isUnlocked) {
if (state.isUnlocked) { if (state.isUnlocked) {
onUnlock() callback.onUnlock()
} }
} }
PinUnlockView( PinUnlockView(

View file

@ -37,9 +37,12 @@ class DefaultLockScreenEntryPointTest {
override fun onSetupDone() = lambdaError() override fun onSetupDone() = lambdaError()
} }
val navTarget = LockScreenEntryPoint.Target.Setup val navTarget = LockScreenEntryPoint.Target.Setup
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null), navTarget) val result = entryPoint.createNode(
.callback(callback) parentNode = parentNode,
.build() buildContext = BuildContext.root(null),
navTarget = navTarget,
callback = callback,
)
assertThat(result).isInstanceOf(LockScreenFlowNode::class.java) assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Setup)) assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Setup))
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)
@ -58,9 +61,12 @@ class DefaultLockScreenEntryPointTest {
override fun onSetupDone() = lambdaError() override fun onSetupDone() = lambdaError()
} }
val navTarget = LockScreenEntryPoint.Target.Settings val navTarget = LockScreenEntryPoint.Target.Settings
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null), navTarget) val result = entryPoint.createNode(
.callback(callback) parentNode = parentNode,
.build() buildContext = BuildContext.root(null),
navTarget = navTarget,
callback = callback,
)
assertThat(result).isInstanceOf(LockScreenFlowNode::class.java) assertThat(result).isInstanceOf(LockScreenFlowNode::class.java)
assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Settings)) assertThat(result.plugins).contains(LockScreenFlowNode.Inputs(LockScreenFlowNode.NavTarget.Settings))
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)

View file

@ -14,6 +14,8 @@ android {
} }
dependencies { dependencies {
implementation(libs.coroutines.core)
api(projects.features.lockscreen.api) api(projects.features.lockscreen.api)
implementation(libs.coroutines.core)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils)
} }

View file

@ -0,0 +1,26 @@
/*
* 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.lockscreen.test
import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeLockScreenEntryPoint : LockScreenEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
navTarget: LockScreenEntryPoint.Target,
callback: LockScreenEntryPoint.Callback,
): Node = lambdaError()
override fun pinUnlockIntent(context: Context): Intent = lambdaError()
}

View file

@ -19,14 +19,13 @@ interface LoginEntryPoint : FeatureEntryPoint {
) )
interface Callback : Plugin { interface Callback : Plugin {
fun onReportProblem() fun navigateToBugReport()
} }
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun createNode(
parentNode: Node,
interface NodeBuilder { buildContext: BuildContext,
fun params(params: Params): NodeBuilder params: Params,
fun callback(callback: Callback): NodeBuilder callback: Callback,
fun build(): Node ): Node
}
} }

View file

@ -9,7 +9,6 @@ package io.element.android.features.login.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginEntryPoint
@ -17,26 +16,21 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultLoginEntryPoint : LoginEntryPoint { class DefaultLoginEntryPoint : LoginEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LoginEntryPoint.NodeBuilder { override fun createNode(
val plugins = ArrayList<Plugin>() parentNode: Node,
buildContext: BuildContext,
return object : LoginEntryPoint.NodeBuilder { params: LoginEntryPoint.Params,
override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder { callback: LoginEntryPoint.Callback,
plugins += LoginFlowNode.Params( ): Node {
return parentNode.createNode<LoginFlowNode>(
buildContext = buildContext,
plugins = listOf(
LoginFlowNode.Params(
accountProvider = params.accountProvider, accountProvider = params.accountProvider,
loginHint = params.loginHint, loginHint = params.loginHint,
) ),
return this callback,
} )
)
override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<LoginFlowNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -18,7 +18,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop import com.bumble.appyx.navmodel.backstack.operation.singleTop
@ -41,6 +40,7 @@ import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTa
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
@ -70,6 +70,7 @@ class LoginFlowNode(
val loginHint: String?, val loginHint: String?,
) : NodeInputs ) : NodeInputs
private val callback: LoginEntryPoint.Callback = callback()
private var activity: Activity? = null private var activity: Activity? = null
private var darkTheme: Boolean = false private var darkTheme: Boolean = false
@ -126,13 +127,13 @@ class LoginFlowNode(
return when (navTarget) { return when (navTarget) {
NavTarget.OnBoarding -> { NavTarget.OnBoarding -> {
val callback = object : OnBoardingNode.Callback { val callback = object : OnBoardingNode.Callback {
override fun onSignUp() { override fun navigateToSignUpFlow() {
backstack.push( backstack.push(
NavTarget.ConfirmAccountProvider(isAccountCreation = true) NavTarget.ConfirmAccountProvider(isAccountCreation = true)
) )
} }
override fun onSignIn(mustChooseAccountProvider: Boolean) { override fun navigateToSignInFlow(mustChooseAccountProvider: Boolean) {
backstack.push( backstack.push(
if (mustChooseAccountProvider) { if (mustChooseAccountProvider) {
NavTarget.ChooseAccountProvider NavTarget.ChooseAccountProvider
@ -142,23 +143,23 @@ class LoginFlowNode(
) )
} }
override fun onSignInWithQrCode() { override fun navigateToQrCode() {
backstack.push(NavTarget.QrCode) backstack.push(NavTarget.QrCode)
} }
override fun onReportProblem() { override fun navigateToBugReport() {
plugins<LoginEntryPoint.Callback>().forEach { it.onReportProblem() } callback.navigateToBugReport()
} }
override fun onOidcDetails(oidcDetails: OidcDetails) { override fun navigateToOidc(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails) navigateToMas(oidcDetails)
} }
override fun onCreateAccountContinue(url: String) { override fun navigateToCreateAccount(url: String) {
backstack.push(NavTarget.CreateAccount(url)) backstack.push(NavTarget.CreateAccount(url))
} }
override fun onLoginPasswordNeeded() { override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword) backstack.push(NavTarget.LoginPassword)
} }
} }
@ -171,15 +172,15 @@ class LoginFlowNode(
} }
NavTarget.ChooseAccountProvider -> { NavTarget.ChooseAccountProvider -> {
val callback = object : ChooseAccountProviderNode.Callback { val callback = object : ChooseAccountProviderNode.Callback {
override fun onOidcDetails(oidcDetails: OidcDetails) { override fun navigateToOidc(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails) navigateToMas(oidcDetails)
} }
override fun onCreateAccountContinue(url: String) { override fun navigateToCreateAccount(url: String) {
backstack.push(NavTarget.CreateAccount(url)) backstack.push(NavTarget.CreateAccount(url))
} }
override fun onLoginPasswordNeeded() { override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword) backstack.push(NavTarget.LoginPassword)
} }
} }
@ -193,19 +194,19 @@ class LoginFlowNode(
isAccountCreation = navTarget.isAccountCreation, isAccountCreation = navTarget.isAccountCreation,
) )
val callback = object : ConfirmAccountProviderNode.Callback { val callback = object : ConfirmAccountProviderNode.Callback {
override fun onOidcDetails(oidcDetails: OidcDetails) { override fun navigateToOidc(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails) navigateToMas(oidcDetails)
} }
override fun onCreateAccountContinue(url: String) { override fun navigateToCreateAccount(url: String) {
backstack.push(NavTarget.CreateAccount(url)) backstack.push(NavTarget.CreateAccount(url))
} }
override fun onLoginPasswordNeeded() { override fun navigateToLoginPassword() {
backstack.push(NavTarget.LoginPassword) backstack.push(NavTarget.LoginPassword)
} }
override fun onChangeAccountProvider() { override fun navigateToChangeAccountProvider() {
backstack.push(NavTarget.ChangeAccountProvider) backstack.push(NavTarget.ChangeAccountProvider)
} }
} }
@ -221,7 +222,7 @@ class LoginFlowNode(
backstack.singleTop(confirmAccountProvider) backstack.singleTop(confirmAccountProvider)
} }
override fun onOtherClick() { override fun navigateToSearchAccountProvider() {
backstack.push(NavTarget.SearchAccountProvider) backstack.push(NavTarget.SearchAccountProvider)
} }
} }

View file

@ -147,11 +147,11 @@ class QrCodeLoginFlowNode(
return when (navTarget) { return when (navTarget) {
is NavTarget.Initial -> { is NavTarget.Initial -> {
val callback = object : QrCodeIntroNode.Callback { val callback = object : QrCodeIntroNode.Callback {
override fun onCancelClicked() { override fun cancel() {
navigateUp() navigateUp()
} }
override fun onContinue() { override fun navigateToQrCodeScan() {
backstack.push(NavTarget.QrCodeScan) backstack.push(NavTarget.QrCodeScan)
} }
} }
@ -159,11 +159,11 @@ class QrCodeLoginFlowNode(
} }
is NavTarget.QrCodeScan -> { is NavTarget.QrCodeScan -> {
val callback = object : QrCodeScanNode.Callback { val callback = object : QrCodeScanNode.Callback {
override fun onScannedCode(qrCodeLoginData: MatrixQrCodeLoginData) { override fun handleScannedCode(qrCodeLoginData: MatrixQrCodeLoginData) {
lifecycleScope.startAuthentication(qrCodeLoginData) lifecycleScope.startAuthentication(qrCodeLoginData)
} }
override fun onCancelClicked() { override fun cancel() {
backstack.pop() backstack.pop()
} }
} }

View file

@ -13,12 +13,12 @@ import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@ -29,16 +29,10 @@ class ChangeAccountProviderNode(
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onDone() fun onDone()
fun onOtherClick() fun navigateToSearchAccountProvider()
} }
private fun onDone() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onDone() }
}
private fun onOtherClick() {
plugins<Callback>().forEach { it.onOtherClick() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -49,8 +43,8 @@ class ChangeAccountProviderNode(
modifier = modifier, modifier = modifier,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onSuccess = ::onDone, onSuccess = callback::onDone,
onOtherProviderClick = ::onOtherClick, onOtherProviderClick = callback::navigateToSearchAccountProvider,
) )
} }
} }

View file

@ -13,12 +13,12 @@ import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -29,22 +29,12 @@ class ChooseAccountProviderNode(
private val presenter: ChooseAccountProviderPresenter, private val presenter: ChooseAccountProviderPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onLoginPasswordNeeded() fun navigateToLoginPassword()
fun onOidcDetails(oidcDetails: OidcDetails) fun navigateToOidc(oidcDetails: OidcDetails)
fun onCreateAccountContinue(url: String) fun navigateToCreateAccount(url: String)
} }
private fun onOidcDetails(oidcDetails: OidcDetails) { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onOidcDetails(oidcDetails) }
}
private fun onLoginPasswordNeeded() {
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
}
private fun onCreateAccountContinue(url: String) {
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -54,10 +44,10 @@ class ChooseAccountProviderNode(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onOidcDetails = ::onOidcDetails, onOidcDetails = callback::navigateToOidc,
onNeedLoginPassword = ::onLoginPasswordNeeded, onNeedLoginPassword = callback::navigateToLoginPassword,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = ::onCreateAccountContinue, onCreateAccountContinue = callback::navigateToCreateAccount,
) )
} }
} }

View file

@ -13,13 +13,13 @@ import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
@ -42,27 +42,13 @@ class ConfirmAccountProviderNode(
) )
interface Callback : Plugin { interface Callback : Plugin {
fun onLoginPasswordNeeded() fun navigateToLoginPassword()
fun onOidcDetails(oidcDetails: OidcDetails) fun navigateToOidc(oidcDetails: OidcDetails)
fun onCreateAccountContinue(url: String) fun navigateToCreateAccount(url: String)
fun onChangeAccountProvider() fun navigateToChangeAccountProvider()
} }
private fun onOidcDetails(data: OidcDetails) { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onOidcDetails(data) }
}
private fun onLoginPasswordNeeded() {
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
}
private fun onCreateAccountContinue(url: String) {
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
}
private fun onChangeAccountProvider() {
plugins<Callback>().forEach { it.onChangeAccountProvider() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -71,10 +57,10 @@ class ConfirmAccountProviderNode(
ConfirmAccountProviderView( ConfirmAccountProviderView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onOidcDetails = ::onOidcDetails, onOidcDetails = callback::navigateToOidc,
onNeedLoginPassword = ::onLoginPasswordNeeded, onNeedLoginPassword = callback::navigateToLoginPassword,
onCreateAccountContinue = ::onCreateAccountContinue, onCreateAccountContinue = callback::navigateToCreateAccount,
onChange = ::onChangeAccountProvider, onChange = callback::navigateToChangeAccountProvider,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
) )
} }

View file

@ -13,13 +13,13 @@ import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
@ -34,13 +34,13 @@ class OnBoardingNode(
plugins = plugins plugins = plugins
) { ) {
interface Callback : Plugin { interface Callback : Plugin {
fun onSignUp() fun navigateToSignUpFlow()
fun onSignIn(mustChooseAccountProvider: Boolean) fun navigateToSignInFlow(mustChooseAccountProvider: Boolean)
fun onSignInWithQrCode() fun navigateToQrCode()
fun onReportProblem() fun navigateToBugReport()
fun onLoginPasswordNeeded() fun navigateToLoginPassword()
fun onOidcDetails(oidcDetails: OidcDetails) fun navigateToOidc(oidcDetails: OidcDetails)
fun onCreateAccountContinue(url: String) fun navigateToCreateAccount(url: String)
} }
data class Params( data class Params(
@ -48,40 +48,13 @@ class OnBoardingNode(
val loginHint: String?, val loginHint: String?,
) : NodeInputs ) : NodeInputs
private val callback: Callback = callback()
private val params = inputs<Params>() private val params = inputs<Params>()
private val presenter = presenterFactory.create( private val presenter = presenterFactory.create(
params = params, params = params,
) )
private fun onSignIn(mustChooseAccountProvider: Boolean) {
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
}
private fun onSignUp() {
plugins<Callback>().forEach { it.onSignUp() }
}
private fun onSignInWithQrCode() {
plugins<Callback>().forEach { it.onSignInWithQrCode() }
}
private fun onReportProblem() {
plugins<Callback>().forEach { it.onReportProblem() }
}
private fun onOidcDetails(data: OidcDetails) {
plugins<Callback>().forEach { it.onOidcDetails(data) }
}
private fun onLoginPasswordNeeded() {
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
}
private fun onCreateAccountContinue(url: String) {
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
@ -89,14 +62,14 @@ class OnBoardingNode(
OnBoardingView( OnBoardingView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onSignIn = ::onSignIn, onSignIn = callback::navigateToSignInFlow,
onCreateAccount = ::onSignUp, onCreateAccount = callback::navigateToSignUpFlow,
onSignInWithQrCode = ::onSignInWithQrCode, onSignInWithQrCode = callback::navigateToQrCode,
onReportProblem = ::onReportProblem, onReportProblem = callback::navigateToBugReport,
onOidcDetails = ::onOidcDetails, onOidcDetails = callback::navigateToOidc,
onNeedLoginPassword = ::onLoginPasswordNeeded, onNeedLoginPassword = callback::navigateToLoginPassword,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = ::onCreateAccountContinue, onCreateAccountContinue = callback::navigateToCreateAccount,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
) )
} }

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
@ContributesNode(QrCodeLoginScope::class) @ContributesNode(QrCodeLoginScope::class)
@ -29,17 +29,14 @@ class QrCodeConfirmationNode(
fun onCancel() fun onCancel()
} }
private val callback: Callback = callback()
private val step = inputs<QrCodeConfirmationStep>() private val step = inputs<QrCodeConfirmationStep>()
private fun onCancel() {
plugins<Callback>().forEach { it.onCancel() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
QrCodeConfirmationView( QrCodeConfirmationView(
step = step, step = step,
onCancel = ::onCancel, onCancel = callback::onCancel,
) )
} }
} }

View file

@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@ -32,10 +32,7 @@ class QrCodeErrorNode(
fun onRetry() fun onRetry()
} }
private fun onRetry() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onRetry() }
}
private val qrCodeErrorScreenType = inputs<QrCodeErrorScreenType>() private val qrCodeErrorScreenType = inputs<QrCodeErrorScreenType>()
@Composable @Composable
@ -44,7 +41,7 @@ class QrCodeErrorNode(
modifier = modifier, modifier = modifier,
errorScreenType = qrCodeErrorScreenType, errorScreenType = qrCodeErrorScreenType,
appName = buildMeta.productionApplicationName, appName = buildMeta.productionApplicationName,
onRetry = ::onRetry, onRetry = callback::onRetry,
) )
} }
} }

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.libraries.architecture.callback
@ContributesNode(QrCodeLoginScope::class) @ContributesNode(QrCodeLoginScope::class)
@AssistedInject @AssistedInject
@ -26,25 +26,19 @@ class QrCodeIntroNode(
private val presenter: QrCodeIntroPresenter, private val presenter: QrCodeIntroPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onCancelClicked() fun cancel()
fun onContinue() fun navigateToQrCodeScan()
} }
private fun onCancelClicked() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onCancelClicked() }
}
private fun onContinue() {
plugins<Callback>().forEach { it.onContinue() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
QrCodeIntroView( QrCodeIntroView(
state = state, state = state,
onBackClick = ::onCancelClicked, onBackClick = callback::cancel,
onContinue = ::onContinue, onContinue = callback::navigateToQrCodeScan,
modifier = modifier modifier = modifier
) )
} }

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
@ContributesNode(QrCodeLoginScope::class) @ContributesNode(QrCodeLoginScope::class)
@ -27,25 +27,19 @@ class QrCodeScanNode(
private val presenter: QrCodeScanPresenter, private val presenter: QrCodeScanPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onScannedCode(qrCodeLoginData: MatrixQrCodeLoginData) fun handleScannedCode(qrCodeLoginData: MatrixQrCodeLoginData)
fun onCancelClicked() fun cancel()
} }
private fun onQrCodeDataReady(qrCodeLoginData: MatrixQrCodeLoginData) { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onScannedCode(qrCodeLoginData) }
}
private fun onCancelClicked() {
plugins<Callback>().forEach { it.onCancelClicked() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
QrCodeScanView( QrCodeScanView(
state = state, state = state,
onQrCodeDataReady = ::onQrCodeDataReady, onQrCodeDataReady = callback::handleScannedCode,
onBackClick = ::onCancelClicked, onBackClick = callback::cancel,
modifier = modifier modifier = modifier
) )
} }

View file

@ -13,12 +13,12 @@ import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@ -31,9 +31,7 @@ class SearchAccountProviderNode(
fun onDone() fun onDone()
} }
private fun onDone() { private val callback: Callback = callback()
plugins<Callback>().forEach { it.onDone() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
@ -44,7 +42,7 @@ class SearchAccountProviderNode(
modifier = modifier, modifier = modifier,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onSuccess = ::onDone, onSuccess = callback::onDone,
) )
} }
} }

View file

@ -39,16 +39,18 @@ class DefaultLoginEntryPointTest {
) )
} }
val callback = object : LoginEntryPoint.Callback { val callback = object : LoginEntryPoint.Callback {
override fun onReportProblem() = lambdaError() override fun navigateToBugReport() = lambdaError()
} }
val params = LoginEntryPoint.Params( val params = LoginEntryPoint.Params(
accountProvider = "ac", accountProvider = "ac",
loginHint = "lh", loginHint = "lh",
) )
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(
.params(params) parentNode = parentNode,
.callback(callback) buildContext = BuildContext.root(null),
.build() params = params,
callback = callback,
)
assertThat(result).isInstanceOf(LoginFlowNode::class.java) assertThat(result).isInstanceOf(LoginFlowNode::class.java)
assertThat(result.plugins).contains(LoginFlowNode.Params(params.accountProvider, params.loginHint)) assertThat(result.plugins).contains(LoginFlowNode.Params(params.accountProvider, params.loginHint))
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)

View file

@ -13,14 +13,13 @@ import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
interface LogoutEntryPoint : FeatureEntryPoint { interface LogoutEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun createNode(
parentNode: Node,
interface NodeBuilder { buildContext: BuildContext,
fun callback(callback: Callback): NodeBuilder callback: Callback,
fun build(): Node ): Node
}
interface Callback : Plugin { interface Callback : Plugin {
fun onChangeRecoveryKeyClick() fun navigateToSecureBackup()
} }
} }

View file

@ -9,7 +9,6 @@ package io.element.android.features.logout.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.logout.api.LogoutEntryPoint
@ -17,18 +16,11 @@ import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultLogoutEntryPoint : LogoutEntryPoint { class DefaultLogoutEntryPoint : LogoutEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LogoutEntryPoint.NodeBuilder { override fun createNode(
val plugins = ArrayList<Plugin>() parentNode: Node,
buildContext: BuildContext,
return object : LogoutEntryPoint.NodeBuilder { callback: LogoutEntryPoint.Callback,
override fun callback(callback: LogoutEntryPoint.Callback): LogoutEntryPoint.NodeBuilder { ): Node {
plugins += callback return parentNode.createNode<LogoutNode>(buildContext, listOf(callback))
return this
}
override fun build(): Node {
return parentNode.createNode<LogoutNode>(buildContext, plugins)
}
}
} }
} }

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -26,16 +26,14 @@ class LogoutNode(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: LogoutPresenter, private val presenter: LogoutPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private fun onChangeRecoveryKeyClick() { private val callback: LogoutEntryPoint.Callback = callback()
plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
LogoutView( LogoutView(
state = state, state = state,
onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick, onChangeRecoveryKeyClick = callback::navigateToSecureBackup,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
modifier = modifier, modifier = modifier,
) )

View file

@ -32,11 +32,13 @@ class DefaultLogoutEntryPointTest {
) )
} }
val callback = object : LogoutEntryPoint.Callback { val callback = object : LogoutEntryPoint.Callback {
override fun onChangeRecoveryKeyClick() = lambdaError() override fun navigateToSecureBackup() = lambdaError()
} }
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.createNode(
.callback(callback) parentNode = parentNode,
.build() buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result).isInstanceOf(LogoutNode::class.java) assertThat(result).isInstanceOf(LogoutNode::class.java)
assertThat(result.plugins).contains(callback) assertThat(result.plugins).contains(callback)
} }

View file

@ -15,6 +15,7 @@ android {
dependencies { dependencies {
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(projects.libraries.architecture)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)
api(projects.features.logout.api) api(projects.features.logout.api)
} }

View file

@ -0,0 +1,21 @@
/*
* 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.logout.test
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
class FakeLogoutEntryPoint : LogoutEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: LogoutEntryPoint.Callback,
): Node = lambdaError()
}

View file

@ -31,23 +31,22 @@ interface MessagesEntryPoint : FeatureEntryPoint {
data object PinnedMessages : InitialTarget data object PinnedMessages : InitialTarget
} }
interface NodeBuilder {
fun params(params: Params): NodeBuilder
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin { interface Callback : Plugin {
fun onRoomDetailsClick() fun navigateToRoomDetails()
fun onUserDataClick(userId: UserId) fun navigateToRoomMemberDetails(userId: UserId)
fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun forwardEvent(eventId: EventId) fun forwardEvent(eventId: EventId)
fun openRoom(roomId: RoomId) fun navigateToRoom(roomId: RoomId)
} }
data class Params(val initialTarget: InitialTarget) : NodeInputs data class Params(val initialTarget: InitialTarget) : NodeInputs
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: Params,
callback: Callback,
): Node
} }
interface MessagesEntryPointNode { interface MessagesEntryPointNode {

View file

@ -79,6 +79,9 @@ dependencies {
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.features.call.test)
testImplementation(projects.features.forward.test)
testImplementation(projects.features.knockrequests.test)
testImplementation(projects.features.location.test) testImplementation(projects.features.location.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.messages.test) testImplementation(projects.features.messages.test)

View file

@ -9,34 +9,20 @@ package io.element.android.features.messages.impl
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultMessagesEntryPoint : MessagesEntryPoint { class DefaultMessagesEntryPoint : MessagesEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MessagesEntryPoint.NodeBuilder { override fun createNode(
val nodeFactories = parentNode.bindings<NodeFactoriesBindings>().nodeFactories() parentNode: Node,
val plugins = ArrayList<Plugin>() buildContext: BuildContext,
params: MessagesEntryPoint.Params,
return object : MessagesEntryPoint.NodeBuilder { callback: MessagesEntryPoint.Callback,
override fun params(params: MessagesEntryPoint.Params): MessagesEntryPoint.NodeBuilder { ): Node {
plugins += MessagesEntryPoint.Params(params.initialTarget) return parentNode.createNode<MessagesFlowNode>(buildContext, listOf(params, callback))
return this
}
override fun callback(callback: MessagesEntryPoint.Callback): MessagesEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return nodeFactories[MessagesFlowNode::class]!!.create(buildContext, plugins)
}
}
} }
} }

View file

@ -16,7 +16,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
@ -55,6 +54,7 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.hide
@ -182,7 +182,7 @@ class MessagesFlowNode(
data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget
} }
private val callbacks = plugins<MessagesEntryPoint.Callback>() private val callback: MessagesEntryPoint.Callback = callback()
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
@ -220,18 +220,18 @@ class MessagesFlowNode(
return when (navTarget) { return when (navTarget) {
is NavTarget.Messages -> { is NavTarget.Messages -> {
val callback = object : MessagesNode.Callback { val callback = object : MessagesNode.Callback {
override fun onRoomDetailsClick() { override fun navigateToRoomDetails() {
callbacks.forEach { it.onRoomDetailsClick() } callback.navigateToRoomDetails()
} }
override fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
return processEventClick( return processEventClick(
timelineMode = timelineMode, timelineMode = timelineMode,
event = event, event = event,
) )
} }
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) { override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
backstack.push( backstack.push(
NavTarget.AttachmentPreview( NavTarget.AttachmentPreview(
attachment = attachments.first(), attachment = attachments.first(),
@ -241,39 +241,39 @@ class MessagesFlowNode(
) )
} }
override fun onUserDataClick(userId: UserId) { override fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.onUserDataClick(userId) } callback.navigateToRoomMemberDetails(userId)
} }
override fun onPermalinkClick(data: PermalinkData) { override fun handlePermalinkClick(data: PermalinkData) {
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack = true) } callback.handlePermalinkClick(data, pushToBackstack = true)
} }
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
} }
override fun onForwardEventClick(eventId: EventId) { override fun forwardEvent(eventId: EventId) {
backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents = false)) backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents = false))
} }
override fun onReportMessage(eventId: EventId, senderId: UserId) { override fun navigateToReportMessage(eventId: EventId, senderId: UserId) {
backstack.push(NavTarget.ReportMessage(eventId, senderId)) backstack.push(NavTarget.ReportMessage(eventId, senderId))
} }
override fun onSendLocationClick() { override fun navigateToSendLocation() {
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live)) backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
} }
override fun onCreatePollClick() { override fun navigateToCreatePoll() {
backstack.push(NavTarget.CreatePoll(Timeline.Mode.Live)) backstack.push(NavTarget.CreatePoll(Timeline.Mode.Live))
} }
override fun onEditPollClick(eventId: EventId) { override fun navigateToEditPoll(eventId: EventId) {
backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId)) backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId))
} }
override fun onJoinCallClick(roomId: RoomId) { override fun navigateToRoomCall(roomId: RoomId) {
val callType = CallType.RoomCall( val callType = CallType.RoomCall(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -282,15 +282,15 @@ class MessagesFlowNode(
elementCallEntryPoint.startCall(callType) elementCallEntryPoint.startCall(callType)
} }
override fun onViewAllPinnedEvents() { override fun navigateToPinnedMessagesList() {
backstack.push(NavTarget.PinnedMessagesList) backstack.push(NavTarget.PinnedMessagesList)
} }
override fun onViewKnockRequests() { override fun navigateToKnockRequestsList() {
backstack.push(NavTarget.KnockRequestsList) backstack.push(NavTarget.KnockRequestsList)
} }
override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) { override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) backstack.push(NavTarget.Thread(threadRootId, focusedEventId))
} }
} }
@ -311,19 +311,21 @@ class MessagesFlowNode(
overlay.hide() overlay.hide()
} }
override fun onViewInTimeline(eventId: EventId) { override fun viewInTimeline(eventId: EventId) {
viewInTimeline(eventId) this@MessagesFlowNode.viewInTimeline(eventId)
} }
override fun onForwardEvent(eventId: EventId) { override fun forwardEvent(eventId: EventId) {
// Need to go to the parent because of the overlay // Need to go to the parent because of the overlay
forwardEvent(eventId) callback.forwardEvent(eventId)
} }
} }
mediaViewerEntryPoint.nodeBuilder(this, buildContext) mediaViewerEntryPoint.createNode(
.params(params) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = params,
callback = callback
)
} }
is NavTarget.AttachmentPreview -> { is NavTarget.AttachmentPreview -> {
val inputs = AttachmentsPreviewNode.Inputs( val inputs = AttachmentsPreviewNode.Inputs(
@ -335,7 +337,11 @@ class MessagesFlowNode(
} }
is NavTarget.LocationViewer -> { is NavTarget.LocationViewer -> {
val inputs = ShowLocationEntryPoint.Inputs(navTarget.location, navTarget.description) val inputs = ShowLocationEntryPoint.Inputs(navTarget.location, navTarget.description)
showLocationEntryPoint.createNode(this, buildContext, inputs) showLocationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
inputs = inputs,
)
} }
is NavTarget.EventDebugInfo -> { is NavTarget.EventDebugInfo -> {
val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo) val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo)
@ -352,70 +358,74 @@ class MessagesFlowNode(
override fun onDone(roomIds: List<RoomId>) { override fun onDone(roomIds: List<RoomId>) {
backstack.pop() backstack.pop()
roomIds.singleOrNull()?.let { roomId -> roomIds.singleOrNull()?.let { roomId ->
callbacks.forEach { it.openRoom(roomId) } callback.navigateToRoom(roomId)
} }
} }
} }
forwardEntryPoint.nodeBuilder(this, buildContext) forwardEntryPoint.createNode(
.params(params) parentNode = this,
.callback(callback) buildContext = buildContext,
.build() params = params,
callback = callback,
)
} }
is NavTarget.ReportMessage -> { is NavTarget.ReportMessage -> {
val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId) val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
createNode<ReportMessageNode>(buildContext, listOf(inputs)) createNode<ReportMessageNode>(buildContext, listOf(inputs))
} }
is NavTarget.SendLocation -> { is NavTarget.SendLocation -> {
sendLocationEntryPoint sendLocationEntryPoint.createNode(
.builder(navTarget.timelineMode) parentNode = this,
.build(this, buildContext) buildContext = buildContext,
timelineMode = navTarget.timelineMode,
)
} }
is NavTarget.CreatePoll -> { is NavTarget.CreatePoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext) createPollEntryPoint.createNode(
.params( parentNode = this,
CreatePollEntryPoint.Params( buildContext = buildContext,
timelineMode = navTarget.timelineMode, params = CreatePollEntryPoint.Params(
mode = CreatePollMode.NewPoll timelineMode = navTarget.timelineMode,
) mode = CreatePollMode.NewPoll
) ),
.build() )
} }
is NavTarget.EditPoll -> { is NavTarget.EditPoll -> {
createPollEntryPoint.nodeBuilder(this, buildContext) createPollEntryPoint.createNode(
.params( parentNode = this,
CreatePollEntryPoint.Params( buildContext = buildContext,
timelineMode = navTarget.timelineMode, params = CreatePollEntryPoint.Params(
mode = CreatePollMode.EditPoll(eventId = navTarget.eventId) timelineMode = navTarget.timelineMode,
) mode = CreatePollMode.EditPoll(eventId = navTarget.eventId)
) ),
.build() )
} }
NavTarget.PinnedMessagesList -> { NavTarget.PinnedMessagesList -> {
val callback = object : PinnedMessagesListNode.Callback { val callback = object : PinnedMessagesListNode.Callback {
override fun onEventClick(event: TimelineItem.Event) { override fun handleEventClick(event: TimelineItem.Event) {
processEventClick( processEventClick(
timelineMode = Timeline.Mode.PinnedEvents, timelineMode = Timeline.Mode.PinnedEvents,
event = event, event = event,
) )
} }
override fun onUserDataClick(userId: UserId) { override fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.onUserDataClick(userId) } callback.navigateToRoomMemberDetails(userId)
} }
override fun onViewInTimelineClick(eventId: EventId) { override fun viewInTimeline(eventId: EventId) {
viewInTimeline(eventId) this@MessagesFlowNode.viewInTimeline(eventId)
} }
override fun onRoomPermalinkClick(data: PermalinkData.RoomLink) { override fun handlePermalinkClick(data: PermalinkData.RoomLink) {
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack = !room.matches(data.roomIdOrAlias)) } callback.handlePermalinkClick(data, pushToBackstack = !room.matches(data.roomIdOrAlias))
} }
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
} }
override fun onForwardEventClick(eventId: EventId) { override fun handleForwardEventClick(eventId: EventId) {
backstack.push(NavTarget.ForwardEvent(eventId = eventId, fromPinnedEvents = true)) backstack.push(NavTarget.ForwardEvent(eventId = eventId, fromPinnedEvents = true))
} }
} }
@ -430,14 +440,14 @@ class MessagesFlowNode(
focusedEventId = navTarget.focusedEventId, focusedEventId = navTarget.focusedEventId,
) )
val callback = object : ThreadedMessagesNode.Callback { val callback = object : ThreadedMessagesNode.Callback {
override fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
return processEventClick( return processEventClick(
timelineMode = timelineMode, timelineMode = timelineMode,
event = event, event = event,
) )
} }
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) { override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
backstack.push( backstack.push(
NavTarget.AttachmentPreview( NavTarget.AttachmentPreview(
attachment = attachments.first(), attachment = attachments.first(),
@ -447,39 +457,39 @@ class MessagesFlowNode(
) )
} }
override fun onUserDataClick(userId: UserId) { override fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.onUserDataClick(userId) } callback.navigateToRoomMemberDetails(userId)
} }
override fun onPermalinkClick(data: PermalinkData) { override fun handlePermalinkClick(data: PermalinkData) {
callbacks.forEach { it.onPermalinkClick(data, pushToBackstack = true) } callback.handlePermalinkClick(data, pushToBackstack = true)
} }
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo))
} }
override fun onForwardEventClick(eventId: EventId) { override fun handleForwardEventClick(eventId: EventId) {
backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents = false)) backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents = false))
} }
override fun onReportMessage(eventId: EventId, senderId: UserId) { override fun navigateToReportMessage(eventId: EventId, senderId: UserId) {
backstack.push(NavTarget.ReportMessage(eventId, senderId)) backstack.push(NavTarget.ReportMessage(eventId, senderId))
} }
override fun onSendLocationClick() { override fun navigateToSendLocation() {
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId))) backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
} }
override fun onCreatePollClick() { override fun navigateToCreatePoll() {
backstack.push(NavTarget.CreatePoll(Timeline.Mode.Thread(navTarget.threadRootId))) backstack.push(NavTarget.CreatePoll(Timeline.Mode.Thread(navTarget.threadRootId)))
} }
override fun onEditPollClick(eventId: EventId) { override fun navigateToEditPoll(eventId: EventId) {
backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId)) backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId))
} }
override fun onJoinCallClick(roomId: RoomId) { override fun navigateToRoomCall(roomId: RoomId) {
val callType = CallType.RoomCall( val callType = CallType.RoomCall(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -488,7 +498,7 @@ class MessagesFlowNode(
elementCallEntryPoint.startCall(callType) elementCallEntryPoint.startCall(callType)
} }
override fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?) { override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) backstack.push(NavTarget.Thread(threadRootId, focusedEventId))
} }
} }
@ -502,11 +512,7 @@ class MessagesFlowNode(
roomIdOrAlias = room.roomId.toRoomIdOrAlias(), roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
eventId = eventId, eventId = eventId,
) )
callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) } callback.handlePermalinkClick(permalinkData, pushToBackstack = false)
}
private fun forwardEvent(eventId: EventId) {
callbacks.forEach { it.forwardEvent(eventId) }
} }
private fun processEventClick( private fun processEventClick(

Some files were not shown because too many files have changed in this diff Show more