Merge remote-tracking branch 'origin/develop' into feature/bma/mutliAccountNotification

This commit is contained in:
Benoit Marty 2025-11-04 16:20:42 +01:00
commit e96cd9e28f
421 changed files with 4365 additions and 4190 deletions

View file

@ -53,7 +53,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs - name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-debug name: elementx-debug
path: | path: |
@ -61,7 +61,7 @@ jobs:
app/build/outputs/apk/fdroid/debug/*-universal-debug.apk app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
- name: Upload x86_64 APK for Maestro - name: Upload x86_64 APK for Maestro
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-apk-maestro name: elementx-apk-maestro
path: | path: |

View file

@ -61,7 +61,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug Enterprise APKs - name: Upload debug Enterprise APKs
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-enterprise-debug name: elementx-enterprise-debug
path: | path: |

View file

@ -20,7 +20,7 @@ jobs:
- run: | - run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger - name: Danger
uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4 uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5
with: with:
args: "--dangerfile ./tools/danger/dangerfile.js" args: "--dangerfile ./tools/danger/dangerfile.js"
env: env:

View file

@ -44,7 +44,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- name: Upload APK as artifact - name: Upload APK as artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-apk-maestro name: elementx-apk-maestro
path: | path: |
@ -69,7 +69,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }} ref: ${{ github.ref }}
- name: Download APK artifact from previous job - name: Download APK artifact from previous job
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
name: elementx-apk-maestro name: elementx-apk-maestro
- name: Enable KVM group perms - name: Enable KVM group perms
@ -102,7 +102,7 @@ jobs:
script: | script: |
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk .github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
- name: Upload test results - name: Upload test results
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: test-results name: test-results
path: | path: |

View file

@ -42,7 +42,7 @@ jobs:
- name: ✅ Upload kover report - name: ✅ Upload kover report
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: kover-results name: kover-results
path: | path: |
@ -74,7 +74,7 @@ jobs:
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
- name: Upload dependency analysis - name: Upload dependency analysis
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: dependency-analysis name: dependency-analysis
path: build/reports/dependency-check-report.html path: build/reports/dependency-check-report.html

View file

@ -97,7 +97,7 @@ jobs:
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: konsist-report name: konsist-report
path: | path: |
@ -174,7 +174,7 @@ jobs:
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: linting-report name: linting-report
path: | path: |
@ -214,7 +214,7 @@ jobs:
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: detekt-report name: detekt-report
path: | path: |
@ -254,7 +254,7 @@ jobs:
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: ktlint-report name: ktlint-report
path: | path: |
@ -317,7 +317,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Download reports from previous jobs - name: Download reports from previous jobs
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
- name: Prepare Danger - name: Prepare Danger
if: always() if: always()
run: | run: |
@ -326,7 +326,7 @@ jobs:
yarn add danger-plugin-lint-report --dev yarn add danger-plugin-lint-report --dev
- name: Danger lint - name: Danger lint
if: always() if: always()
uses: danger/danger-js@bdccecb77e0144055fbaea9224f10cf8b1229b68 # 13.0.4 uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5
with: with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js" args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env: env:

View file

@ -38,7 +38,7 @@ jobs:
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact - name: Upload bundle as artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-app-gplay-bundle-unsigned name: elementx-app-gplay-bundle-unsigned
path: | path: |
@ -74,7 +74,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact - name: Upload bundle as artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-enterprise-app-gplay-bundle-unsigned name: elementx-enterprise-app-gplay-bundle-unsigned
path: | path: |
@ -102,7 +102,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact - name: Upload apks as artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: elementx-app-fdroid-apks-unsigned name: elementx-app-fdroid-apks-unsigned
path: | path: |

View file

@ -61,7 +61,7 @@ jobs:
- name: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: kover-error-report name: kover-error-report
path: | path: |
@ -73,7 +73,7 @@ jobs:
- name: 🚫 Upload test results on error - name: 🚫 Upload test results on error
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: tests-and-screenshot-tests-results name: tests-and-screenshot-tests-results
path: | path: |

View file

@ -1,3 +1,12 @@
Changes in Element X v25.11.0
=============================
Hotfix release.
Includes https://github.com/element-hq/element-x-android/pull/5615, which fixes an issue that prevented Element Call notifications from being displayed sometimes.
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.10.1...v25.11.0
Changes in Element X v25.10.1 Changes in Element X v25.10.1
============================= =============================

View file

@ -9,13 +9,14 @@ package io.element.android.x.di
import dev.zacsweers.metro.GraphExtension import dev.zacsweers.metro.GraphExtension
import dev.zacsweers.metro.Provides import dev.zacsweers.metro.Provides
import io.element.android.appnav.di.TimelineBindings
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
@GraphExtension(RoomScope::class) @GraphExtension(RoomScope::class)
interface RoomGraph : NodeFactoriesBindings { interface RoomGraph : NodeFactoriesBindings, TimelineBindings {
@GraphExtension.Factory @GraphExtension.Factory
interface Factory { interface Factory {
fun create( fun create(

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.navigateToBugReport()
} }
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,
)
} }
} }
} }
@ -339,7 +349,7 @@ class RootFlowNode(
} else { } else {
// wait for the current session to be restored // wait for the current session to be restored
val loggedInFlowNode = attachSession(latestSessionId) val loggedInFlowNode = attachSession(latestSessionId)
if (sessionStore.getAllSessions().size > 1) { if (sessionStore.numberOfSessions() > 1) {
// Several accounts, let the user choose which one to use // Several accounts, let the user choose which one to use
backstack.push( backstack.push(
NavTarget.AccountSelect( NavTarget.AccountSelect(
@ -369,7 +379,7 @@ class RootFlowNode(
is PermalinkData.FallbackLink -> Unit is PermalinkData.FallbackLink -> Unit
is PermalinkData.RoomEmailInviteLink -> Unit is PermalinkData.RoomEmailInviteLink -> Unit
else -> { else -> {
if (sessionStore.getAllSessions().size > 1) { if (sessionStore.numberOfSessions() > 1) {
// Several accounts, let the user choose which one to use // Several accounts, let the user choose which one to use
backstack.push( backstack.push(
NavTarget.AccountSelect( NavTarget.AccountSelect(

View file

@ -0,0 +1,16 @@
/*
* 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.di
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
interface TimelineBindings {
val timelineProvider: TimelineProvider
val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
}

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

@ -22,15 +22,16 @@ 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.appnav.di.TimelineBindings
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.messages.api.MessagesEntryPointNode
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
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
@ -46,8 +47,6 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -76,9 +75,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 +86,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 +122,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, fromPinnedEvents: Boolean) {
backstack.push(NavTarget.ForwardEvent(eventId)) backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
} }
} }
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 {
@ -166,42 +167,50 @@ class JoinedRoomLoadedFlowNode(
createSpaceNode(buildContext) createSpaceNode(buildContext)
} }
is NavTarget.ForwardEvent -> { is NavTarget.ForwardEvent -> {
val timelineProvider = { MutableStateFlow(inputs.room.liveTimeline).asStateFlow() } val timelineProvider = if (navTarget.fromPinnedEvents) {
(graph as TimelineBindings).pinnedEventsTimelineProvider
} else {
(graph as TimelineBindings).timelineProvider
}
val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
val callback = object : ForwardEntryPoint.Callback { val callback = object : ForwardEntryPoint.Callback {
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 +218,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, fromPinnedEvents: Boolean) {
backstack.push(NavTarget.ForwardEvent(eventId)) backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents))
} }
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 {
@ -257,7 +268,7 @@ class JoinedRoomLoadedFlowNode(
data class RoomMemberDetails(val userId: UserId) : NavTarget data class RoomMemberDetails(val userId: UserId) : NavTarget
@Parcelize @Parcelize
data class ForwardEvent(val eventId: EventId) : NavTarget data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget
@Parcelize @Parcelize
data object RoomNotificationSettings : NavTarget data object RoomNotificationSettings : NavTarget
@ -267,7 +278,7 @@ class JoinedRoomLoadedFlowNode(
val messageNode = waitForChildAttached<Node, NavTarget> { navTarget -> val messageNode = waitForChildAttached<Node, NavTarget> { navTarget ->
navTarget is NavTarget.Messages navTarget is NavTarget.Messages
} }
(messageNode as? MessagesEntryPointNode)?.attachThread(threadId, focusedEventId) (messageNode as? MessagesEntryPoint.NodeProxy)?.attachThread(threadId, focusedEventId)
} }
@Composable @Composable

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

@ -0,0 +1,2 @@
Main changes in this version: fixes an issue that prevented Element Call notifications from being displayed sometimes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -201,7 +201,7 @@ class CallScreenPresenter(
userAgent = userAgent, userAgent = userAgent,
isCallActive = isWidgetLoaded, isCallActive = isWidgetLoaded,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = { handleEvents(it) }, eventSink = ::handleEvents,
) )
} }

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

@ -33,7 +33,7 @@
<string name="screen_room_change_role_section_users">"Mitglieder"</string> <string name="screen_room_change_role_section_users">"Mitglieder"</string>
<string name="screen_room_change_role_unsaved_changes_description">"Du hast nicht gespeicherte Änderungen."</string> <string name="screen_room_change_role_unsaved_changes_description">"Du hast nicht gespeicherte Änderungen."</string>
<string name="screen_room_change_role_unsaved_changes_title">"Änderungen speichern?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Änderungen speichern?"</string>
<string name="screen_room_member_list_banned_empty">"In diesem Chat gibt es keine gesperrten Nutzer."</string> <string name="screen_room_member_list_banned_empty">"Es gibt keine gesperrten Nutzer."</string>
<plurals name="screen_room_member_list_header_title"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d Person"</item> <item quantity="one">"%1$d Person"</item>
<item quantity="other">"%1$d Personen"</item> <item quantity="other">"%1$d Personen"</item>

View file

@ -33,7 +33,7 @@
<string name="screen_room_change_role_section_users">"Liikmed"</string> <string name="screen_room_change_role_section_users">"Liikmed"</string>
<string name="screen_room_change_role_unsaved_changes_description">"Sul on salvestamata muudatusi"</string> <string name="screen_room_change_role_unsaved_changes_description">"Sul on salvestamata muudatusi"</string>
<string name="screen_room_change_role_unsaved_changes_title">"Kas salvestame muudatused?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Kas salvestame muudatused?"</string>
<string name="screen_room_member_list_banned_empty">"Jututoas pole suhtluskeeluga kasutajaid"</string> <string name="screen_room_member_list_banned_empty">"Suhtluskeeluga kasutajaid pole"</string>
<plurals name="screen_room_member_list_header_title"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d osaleja"</item> <item quantity="one">"%1$d osaleja"</item>
<item quantity="other">"%1$d osalejat"</item> <item quantity="other">"%1$d osalejat"</item>

View file

@ -33,7 +33,7 @@
<string name="screen_room_change_role_section_users">"Členovia"</string> <string name="screen_room_change_role_section_users">"Členovia"</string>
<string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené zmeny."</string> <string name="screen_room_change_role_unsaved_changes_description">"Máte neuložené zmeny."</string>
<string name="screen_room_change_role_unsaved_changes_title">"Uložiť zmeny?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Uložiť zmeny?"</string>
<string name="screen_room_member_list_banned_empty">"V tejto miestnosti nie sú žiadni zakázaní používatelia."</string> <string name="screen_room_member_list_banned_empty">"Neexistujú žiadni zablokovaní používatelia."</string>
<plurals name="screen_room_member_list_header_title"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d osoba"</item> <item quantity="one">"%1$d osoba"</item>
<item quantity="few">"%1$d osoby"</item> <item quantity="few">"%1$d osoby"</item>

View file

@ -33,7 +33,7 @@
<string name="screen_room_change_role_section_users">"成員"</string> <string name="screen_room_change_role_section_users">"成員"</string>
<string name="screen_room_change_role_unsaved_changes_description">"您有尚未儲存的變更"</string> <string name="screen_room_change_role_unsaved_changes_description">"您有尚未儲存的變更"</string>
<string name="screen_room_change_role_unsaved_changes_title">"是否儲存變更?"</string> <string name="screen_room_change_role_unsaved_changes_title">"是否儲存變更?"</string>
<string name="screen_room_member_list_banned_empty">"此聊天室沒有黑名單。"</string> <string name="screen_room_member_list_banned_empty">"沒有被封鎖的使用者。"</string>
<plurals name="screen_room_member_list_header_title"> <plurals name="screen_room_member_list_header_title">
<item quantity="other">"%1$d 位夥伴"</item> <item quantity="other">"%1$d 位夥伴"</item>
</plurals> </plurals>

View file

@ -33,7 +33,7 @@
<string name="screen_room_change_role_section_users">"Members"</string> <string name="screen_room_change_role_section_users">"Members"</string>
<string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string> <string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string>
<string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string>
<string name="screen_room_member_list_banned_empty">"There are no banned users in this room."</string> <string name="screen_room_member_list_banned_empty">"There are no banned users."</string>
<plurals name="screen_room_member_list_header_title"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d person"</item> <item quantity="one">"%1$d person"</item>
<item quantity="other">"%1$d people"</item> <item quantity="other">"%1$d people"</item>

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

@ -31,8 +31,10 @@ dependencies {
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.roomselect.api) implementation(projects.libraries.roomselect.api)
implementation(projects.libraries.uiStrings)
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

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
@AssistedInject @AssistedInject
class ForwardMessagesPresenter( class ForwardMessagesPresenter(
@ -54,7 +55,7 @@ class ForwardMessagesPresenter(
return ForwardMessagesState( return ForwardMessagesState(
forwardAction = forwardingActionState.value, forwardAction = forwardingActionState.value,
eventSink = { handleEvents(it) } eventSink = ::handleEvents,
) )
} }
@ -63,7 +64,11 @@ class ForwardMessagesPresenter(
roomIds: List<RoomId>, roomIds: List<RoomId>,
) = launch { ) = launch {
suspend { suspend {
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).getOrThrow() timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds)
.onFailure {
Timber.e(it, "Error while forwarding event")
}
.getOrThrow()
roomIds roomIds
}.runCatchingUpdatingState(forwardingActionState) }.runCatchingUpdatingState(forwardingActionState)
} }

View file

@ -8,11 +8,13 @@
package io.element.android.features.forward.impl package io.element.android.features.forward.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun ForwardMessagesView( fun ForwardMessagesView(
@ -24,6 +26,9 @@ fun ForwardMessagesView(
onSuccess = { onSuccess = {
onForwardSuccess(it) onForwardSuccess(it)
}, },
errorMessage = {
stringResource(id = CommonStrings.error_unknown)
},
onErrorDismiss = { onErrorDismiss = {
state.eventSink(ForwardMessagesEvents.ClearError) state.eventSink(ForwardMessagesEvents.ClearError)
}, },

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

@ -13,5 +13,4 @@ data class AccountProvider(
val subtitle: String? = null, val subtitle: String? = null,
val isPublic: Boolean = false, val isPublic: Boolean = false,
val isMatrixOrg: Boolean = false, val isMatrixOrg: Boolean = false,
val isValid: Boolean = false,
) )

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