Ensure a Callback and only one is provided in the Plugin. Also reduce boilerplate code in Nodes.

This commit is contained in:
Benoit Marty 2025-10-30 09:14:41 +01:00 committed by Benoit Marty
parent 2e8785b36b
commit be03c50aaf
76 changed files with 374 additions and 741 deletions

View file

@ -17,7 +17,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class)
@AssistedInject
@ -26,23 +26,15 @@ class AccountSelectNode(
@Assisted plugins: List<Plugin>,
private val presenter: AccountSelectPresenter,
) : Node(buildContext, plugins = plugins) {
private val callbacks = plugins.filterIsInstance<AccountSelectEntryPoint.Callback>()
private fun onDismiss() {
callbacks.forEach { it.onCancel() }
}
private fun onAccountSelected(sessionId: SessionId) {
callbacks.forEach { it.onAccountSelected(sessionId) }
}
private val callback: AccountSelectEntryPoint.Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AccountSelectView(
state = state,
onDismiss = ::onDismiss,
onSelectAccount = ::onAccountSelected,
onDismiss = callback::onCancel,
onSelectAccount = callback::onAccountSelected,
modifier = modifier,
)
}

View file

@ -0,0 +1,16 @@
/*
* Copyright 2023, 2024 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.libraries.architecture
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
inline fun <reified I : Plugin> Node.callback(): I {
return requireNotNull(plugins<I>().singleOrNull()) { "Make sure to actually pass a Callback plugin to your node" }
}

View file

@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
@ -43,28 +43,14 @@ class MediaGalleryNode(
fun forward(eventId: EventId)
}
private fun onBackClick() {
plugins<Callback>().forEach {
it.onBackClick()
}
}
private val callback: Callback = callback()
override fun onViewInTimelineClick(eventId: EventId) {
plugins<Callback>().forEach {
it.viewInTimeline(eventId)
}
callback.viewInTimeline(eventId)
}
override fun onForwardClick(eventId: EventId) {
plugins<Callback>().forEach {
it.forward(eventId)
}
}
private fun onItemClick(item: MediaItem.Event) {
plugins<Callback>().forEach {
it.showItem(item)
}
callback.forward(eventId)
}
@Composable
@ -75,8 +61,8 @@ class MediaGalleryNode(
val state = presenter.present()
MediaGalleryView(
state = state,
onBackClick = ::onBackClick,
onItemClick = ::onItemClick,
onBackClick = callback::onBackClick,
onItemClick = callback::showItem,
modifier = modifier,
)
}

View file

@ -13,13 +13,13 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.BackstackWithOverlayBox
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.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.hide
@ -70,38 +70,22 @@ class MediaGalleryFlowNode(
) : NavTarget
}
private fun onBackClick() {
plugins<MediaGalleryEntryPoint.Callback>().forEach {
it.onBackClick()
}
}
private fun onViewInTimeline(eventId: EventId) {
plugins<MediaGalleryEntryPoint.Callback>().forEach {
it.viewInTimeline(eventId)
}
}
private fun forwardEvent(eventId: EventId) {
plugins<MediaGalleryEntryPoint.Callback>().forEach {
it.forward(eventId)
}
}
private val callback: MediaGalleryEntryPoint.Callback = callback()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> {
val callback = object : MediaGalleryNode.Callback {
override fun onBackClick() {
this@MediaGalleryFlowNode.onBackClick()
callback.onBackClick()
}
override fun viewInTimeline(eventId: EventId) {
this@MediaGalleryFlowNode.onViewInTimeline(eventId)
callback.viewInTimeline(eventId)
}
override fun forward(eventId: EventId) {
forwardEvent(eventId)
callback.forward(eventId)
}
override fun showItem(item: MediaItem.Event) {
@ -132,12 +116,12 @@ class MediaGalleryFlowNode(
}
override fun viewInTimeline(eventId: EventId) {
this@MediaGalleryFlowNode.onViewInTimeline(eventId)
callback.viewInTimeline(eventId)
}
override fun forwardEvent(eventId: EventId) {
// Need to go to the parent because of the overlay
this@MediaGalleryFlowNode.forwardEvent(eventId)
callback.forward(eventId)
}
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)

View file

@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
@ -23,6 +22,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.viewfolder.api.TextFileViewer
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -57,28 +57,19 @@ class MediaViewerNode(
private val enterpriseService: EnterpriseService,
) : Node(buildContext, plugins = plugins),
MediaViewerNavigator {
private val callback: MediaViewerEntryPoint.Callback = callback()
private val inputs = inputs<MediaViewerEntryPoint.Params>()
private fun onDone() {
plugins<MediaViewerEntryPoint.Callback>().forEach {
it.onDone()
}
}
override fun onViewInTimelineClick(eventId: EventId) {
plugins<MediaViewerEntryPoint.Callback>().forEach {
it.viewInTimeline(eventId)
}
callback.viewInTimeline(eventId)
}
override fun onForwardClick(eventId: EventId) {
plugins<MediaViewerEntryPoint.Callback>().forEach {
it.forwardEvent(eventId)
}
callback.forwardEvent(eventId)
}
override fun onItemDeleted() {
onDone()
callback.onDone()
}
private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) {
@ -153,7 +144,7 @@ class MediaViewerNode(
textFileViewer = textFileViewer,
modifier = modifier,
audioFocus = audioFocus,
onBackClick = ::onDone,
onBackClick = callback::onDone,
)
}
}

View file

@ -62,7 +62,7 @@ class IgnoredUsersTest(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
navigator.openIgnoredUsers()
navigator.navigateToBlockedUsers()
}
override suspend fun reset() = delegate.reset()

View file

@ -39,7 +39,7 @@ class IgnoredUsersTestTest {
)
val openIgnoredUsersResult = lambdaRecorder<Unit> {}
val navigator = object : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = openIgnoredUsersResult()
override fun navigateToBlockedUsers() = openIgnoredUsersResult()
}
sut.quickFix(
coroutineScope = backgroundScope,

View file

@ -16,9 +16,9 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
import io.element.android.libraries.roomselect.api.RoomSelectMode
@ -35,24 +35,15 @@ class RoomSelectNode(
private val inputs: Inputs = inputs()
private val presenter = presenterFactory.create(inputs.mode)
private val callbacks = plugins.filterIsInstance<RoomSelectEntryPoint.Callback>()
private fun onDismiss() {
callbacks.forEach { it.onCancel() }
}
private fun onRoomSelected(roomIds: List<RoomId>) {
callbacks.forEach { it.onRoomSelected(roomIds) }
}
private val callback: RoomSelectEntryPoint.Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
RoomSelectView(
state = state,
onDismiss = ::onDismiss,
onSubmit = ::onRoomSelected,
onDismiss = callback::onCancel,
onSubmit = callback::onRoomSelected,
modifier = modifier
)
}

View file

@ -8,5 +8,5 @@
package io.element.android.libraries.troubleshoot.api.test
interface NotificationTroubleshootNavigator {
fun openIgnoredUsers()
fun navigateToBlockedUsers()
}

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
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.troubleshoot.api.NotificationTroubleShootEntryPoint
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
@ -31,20 +31,13 @@ class TroubleshootNotificationsNode(
factory: TroubleshootNotificationsPresenter.Factory,
) : Node(buildContext, plugins = plugins),
NotificationTroubleshootNavigator {
private val callback: NotificationTroubleShootEntryPoint.Callback = callback()
private val presenter = factory.create(
navigator = this,
)
private fun onDone() {
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
it.onDone()
}
}
override fun openIgnoredUsers() {
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
it.navigateToBlockedUsers()
}
override fun navigateToBlockedUsers() {
callback.navigateToBlockedUsers()
}
@Composable
@ -53,7 +46,7 @@ class TroubleshootNotificationsNode(
val state = presenter.present()
TroubleshootNotificationsView(
state = state,
onBackClick = ::onDone,
onBackClick = callback::onDone,
modifier = modifier,
)
}

View file

@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
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.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@ -31,16 +31,10 @@ class PushHistoryNode(
presenterFactory: PushHistoryPresenter.Factory,
private val screenTracker: ScreenTracker,
) : Node(buildContext, plugins = plugins), PushHistoryNavigator {
private fun onDone() {
plugins<PushHistoryEntryPoint.Callback>().forEach {
it.onDone()
}
}
private val callback: PushHistoryEntryPoint.Callback = callback()
override fun navigateTo(roomId: RoomId, eventId: EventId) {
plugins<PushHistoryEntryPoint.Callback>().forEach {
it.navigateToEvent(roomId, eventId)
}
callback.navigateToEvent(roomId, eventId)
}
private val presenter = presenterFactory.create(this)
@ -51,7 +45,7 @@ class PushHistoryNode(
val state = presenter.present()
PushHistoryView(
state = state,
onBackClick = ::onDone,
onBackClick = callback::onDone,
modifier = modifier,
)
}

View file

@ -180,7 +180,7 @@ private fun createTroubleshootTestSuite(
internal fun createTroubleshootNotificationsPresenter(
navigator: NotificationTroubleshootNavigator = object : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = lambdaError()
override fun navigateToBlockedUsers() = lambdaError()
},
troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(),
): TroubleshootNotificationsPresenter {

View file

@ -13,5 +13,5 @@ import io.element.android.tests.testutils.lambda.lambdaError
class FakeNotificationTroubleshootNavigator(
private val openIgnoredUsersResult: () -> Unit = { lambdaError() },
) : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = openIgnoredUsersResult()
override fun navigateToBlockedUsers() = openIgnoredUsersResult()
}