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

@ -16,7 +16,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
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 com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
@ -55,6 +54,7 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.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
@ -182,7 +182,7 @@ class MessagesFlowNode(
data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget
}
private val callbacks = plugins<MessagesEntryPoint.Callback>()
private val callback: MessagesEntryPoint.Callback = callback()
override fun onBuilt() {
super.onBuilt()
@ -221,7 +221,7 @@ class MessagesFlowNode(
is NavTarget.Messages -> {
val callback = object : MessagesNode.Callback {
override fun navigateToRoomDetails() {
callbacks.forEach { it.navigateToRoomDetails() }
callback.navigateToRoomDetails()
}
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
@ -242,11 +242,11 @@ class MessagesFlowNode(
}
override fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.navigateToRoomMemberDetails(userId) }
callback.navigateToRoomMemberDetails(userId)
}
override fun handlePermalinkClick(data: PermalinkData) {
callbacks.forEach { it.handlePermalinkClick(data, pushToBackstack = true) }
callback.handlePermalinkClick(data, pushToBackstack = true)
}
override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
@ -317,7 +317,7 @@ class MessagesFlowNode(
override fun forwardEvent(eventId: EventId) {
// Need to go to the parent because of the overlay
callbacks.forEach { it.forwardEvent(eventId) }
callback.forwardEvent(eventId)
}
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
@ -352,7 +352,7 @@ class MessagesFlowNode(
override fun onDone(roomIds: List<RoomId>) {
backstack.pop()
roomIds.singleOrNull()?.let { roomId ->
callbacks.forEach { it.navigateToRoom(roomId) }
callback.navigateToRoom(roomId)
}
}
}
@ -400,7 +400,7 @@ class MessagesFlowNode(
}
override fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.navigateToRoomMemberDetails(userId) }
callback.navigateToRoomMemberDetails(userId)
}
override fun viewInTimeline(eventId: EventId) {
@ -408,7 +408,7 @@ class MessagesFlowNode(
}
override fun handlePermalinkClick(data: PermalinkData.RoomLink) {
callbacks.forEach { it.handlePermalinkClick(data, pushToBackstack = !room.matches(data.roomIdOrAlias)) }
callback.handlePermalinkClick(data, pushToBackstack = !room.matches(data.roomIdOrAlias))
}
override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
@ -448,11 +448,11 @@ class MessagesFlowNode(
}
override fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.navigateToRoomMemberDetails(userId) }
callback.navigateToRoomMemberDetails(userId)
}
override fun handlePermalinkClick(data: PermalinkData) {
callbacks.forEach { it.handlePermalinkClick(data, pushToBackstack = true) }
callback.handlePermalinkClick(data, pushToBackstack = true)
}
override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
@ -502,7 +502,7 @@ class MessagesFlowNode(
roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
eventId = eventId,
)
callbacks.forEach { it.handlePermalinkClick(permalinkData, pushToBackstack = false) }
callback.handlePermalinkClick(permalinkData, pushToBackstack = false)
}
private fun processEventClick(

View file

@ -24,7 +24,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
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
@ -48,8 +47,8 @@ import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTa
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.androidutils.system.toast
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.core.bool.orFalse
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.ApplicationContext
@ -93,13 +92,12 @@ class MessagesNode(
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val callbacks = plugins<Callback>()
data class Inputs(
val focusedEventId: EventId?,
) : NodeInputs
private val inputs = inputs<Inputs>()
private val callback: Callback = callback()
private val timelineController = TimelineController(room, room.liveTimeline)
private val presenter = presenterFactory.create(
@ -143,32 +141,6 @@ class MessagesNode(
)
}
private fun navigateToRoomDetails() {
callbacks.forEach { it.navigateToRoomDetails() }
}
private fun navigateToPinnedMessagesList() {
callbacks.forEach { it.navigateToPinnedMessagesList() }
}
private fun navigateToKnockRequestsList() {
callbacks.forEach { it.navigateToKnockRequestsList() }
}
private fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
// Note: cannot use `callbacks.all { it.onEventClick(event) }` because:
// - if callbacks is empty, it will return true and we want to return false.
// - if a callback returns false, the other callback will not be invoked.
return callbacks.takeIf { it.isNotEmpty() }
?.map { it.handleEventClick(timelineMode, event) }
?.all { it }
.orFalse()
}
private fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.navigateToRoomMemberDetails(userId) }
}
private fun onLinkClick(
activity: Activity,
darkTheme: Boolean,
@ -180,7 +152,7 @@ class MessagesNode(
is PermalinkData.UserLink -> {
// Open the room member profile, it will fallback to
// the user profile if the user is not in the room
callbacks.forEach { it.navigateToRoomMemberDetails(permalink.userId) }
callback.navigateToRoomMemberDetails(permalink.userId)
}
is PermalinkData.RoomLink -> {
handleRoomLinkClick(permalink, eventSink)
@ -211,28 +183,28 @@ class MessagesNode(
displaySameRoomToast()
}
} else {
callbacks.forEach { it.handlePermalinkClick(roomLink) }
callback.handlePermalinkClick(roomLink)
}
}
override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
callbacks.forEach { it.navigateToEventDebugInfo(eventId, debugInfo) }
callback.navigateToEventDebugInfo(eventId, debugInfo)
}
override fun forwardEvent(eventId: EventId) {
callbacks.forEach { it.forwardEvent(eventId) }
callback.forwardEvent(eventId)
}
override fun navigateToReportMessage(eventId: EventId, senderId: UserId) {
callbacks.forEach { it.navigateToReportMessage(eventId, senderId) }
callback.navigateToReportMessage(eventId, senderId)
}
override fun navigateToEditPoll(eventId: EventId) {
callbacks.forEach { it.navigateToEditPoll(eventId) }
callback.navigateToEditPoll(eventId)
}
override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
callbacks.forEach { it.navigateToPreviewAttachments(attachments, inReplyToEventId) }
callback.navigateToPreviewAttachments(attachments, inReplyToEventId)
}
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>) {
@ -240,24 +212,12 @@ class MessagesNode(
displaySameRoomToast()
} else {
val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), eventId, viaParameters = serverNames.toImmutableList())
callbacks.forEach { it.handlePermalinkClick(permalinkData) }
callback.handlePermalinkClick(permalinkData)
}
}
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
callbacks.forEach { it.navigateToThread(threadRootId, focusedEventId) }
}
private fun navigateToSendLocation() {
callbacks.forEach { it.navigateToSendLocation() }
}
private fun navigateToCreatePoll() {
callbacks.forEach { it.navigateToCreatePoll() }
}
private fun navigateToRoomCall() {
callbacks.forEach { it.navigateToRoomCall(room.roomId) }
callback.navigateToThread(threadRootId, focusedEventId)
}
private fun displaySameRoomToast() {
@ -288,20 +248,20 @@ class MessagesNode(
MessagesView(
state = state,
onBackClick = { state.eventSink(MessagesEvents.MarkAsFullyReadAndExit) },
onRoomDetailsClick = ::navigateToRoomDetails,
onRoomDetailsClick = callback::navigateToRoomDetails,
onEventContentClick = { isLive, event ->
if (isLive) {
onEventClick(timelineController.mainTimelineMode(), event)
callback.handleEventClick(timelineController.mainTimelineMode(), event)
} else {
val detachedTimelineMode = timelineController.detachedTimelineMode()
if (detachedTimelineMode != null) {
onEventClick(detachedTimelineMode, event)
callback.handleEventClick(detachedTimelineMode, event)
} else {
false
}
}
},
onUserDataClick = ::navigateToRoomMemberDetails,
onUserDataClick = callback::navigateToRoomMemberDetails,
onLinkClick = { url, customTab ->
onLinkClick(
activity = activity,
@ -311,15 +271,15 @@ class MessagesNode(
customTab = customTab,
)
},
onSendLocationClick = ::navigateToSendLocation,
onCreatePollClick = ::navigateToCreatePoll,
onJoinCallClick = ::navigateToRoomCall,
onViewAllPinnedMessagesClick = ::navigateToPinnedMessagesList,
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList,
modifier = modifier,
knockRequestsBannerView = {
knockRequestsBannerRenderer.View(
modifier = Modifier,
onViewRequestsClick = ::navigateToKnockRequestsList,
onViewRequestsClick = callback::navigateToKnockRequestsList,
)
},
)
@ -327,7 +287,7 @@ class MessagesNode(
state = state.roomMemberModerationState,
onSelectAction = { action, target ->
when (action) {
is ModerationAction.DisplayProfile -> navigateToRoomMemberDetails(target.userId)
is ModerationAction.DisplayProfile -> callback.navigateToRoomMemberDetails(target.userId)
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
}
},

View file

@ -17,7 +17,6 @@ import androidx.compose.ui.platform.LocalView
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
@ -27,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.di.TimelineItemPresent
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
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.matrix.api.core.UserId
@ -34,7 +34,6 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@ContributesNode(RoomScope::class)
@ -56,6 +55,7 @@ class PinnedMessagesListNode(
fun handleForwardEventClick(eventId: EventId)
}
private val callback: Callback = callback()
private val presenter = presenterFactory.create(
navigator = this,
actionListPresenter = actionListPresenterFactory.create(
@ -63,25 +63,16 @@ class PinnedMessagesListNode(
timelineMode = Timeline.Mode.PinnedEvents,
)
)
private val callbacks = plugins<Callback>()
private fun handleEventClick(event: TimelineItem.Event) {
return callbacks.forEach { it.handleEventClick(event) }
}
private fun navigateToRoomMemberDetails(user: MatrixUser) {
callbacks.forEach { it.navigateToRoomMemberDetails(user.userId) }
}
private fun onLinkClick(context: Context, url: String) {
when (val permalink = permalinkParser.parse(url)) {
is PermalinkData.UserLink -> {
// Open the room member profile, it will fallback to
// the user profile if the user is not in the room
callbacks.forEach { it.navigateToRoomMemberDetails(permalink.userId) }
callback.navigateToRoomMemberDetails(permalink.userId)
}
is PermalinkData.RoomLink -> {
callbacks.forEach { it.handlePermalinkClick(permalink) }
callback.handlePermalinkClick(permalink)
}
is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> {
@ -91,15 +82,15 @@ class PinnedMessagesListNode(
}
override fun viewInTimeline(eventId: EventId) {
callbacks.forEach { it.viewInTimeline(eventId) }
callback.viewInTimeline(eventId)
}
override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
callbacks.forEach { it.navigateToEventDebugInfo(eventId, debugInfo) }
callback.navigateToEventDebugInfo(eventId, debugInfo)
}
override fun forwardEvent(eventId: EventId) {
callbacks.forEach { it.handleForwardEventClick(eventId) }
callback.handleForwardEventClick(eventId)
}
@Composable
@ -113,8 +104,8 @@ class PinnedMessagesListNode(
PinnedMessagesListView(
state = state,
onBackClick = ::navigateUp,
onEventClick = ::handleEventClick,
onUserDataClick = ::navigateToRoomMemberDetails,
onEventClick = callback::handleEventClick,
onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) },
onLinkClick = { link -> onLinkClick(context, link.url) },
onLinkLongClick = {
view.performHapticFeedback(

View file

@ -22,7 +22,6 @@ import com.bumble.appyx.core.lifecycle.subscribe
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
@ -44,8 +43,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
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.core.bool.orFalse
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
@ -88,14 +87,13 @@ class ThreadedMessagesNode(
private val permalinkParser: PermalinkParser,
private val appNavigationStateService: AppNavigationStateService,
) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val callbacks = plugins<Callback>()
data class Inputs(
val threadRootEventId: ThreadId,
val focusedEventId: EventId?,
) : NodeInputs
private val inputs = inputs<Inputs>()
private val callback: Callback = callback()
// TODO use a loading state node to preload this instead of using `runBlocking`
private val threadedTimeline = runBlocking { room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() }
@ -145,20 +143,6 @@ class ThreadedMessagesNode(
)
}
private fun onEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
// Note: cannot use `callbacks.all { it.onEventClick(event) }` because:
// - if callbacks is empty, it will return true and we want to return false.
// - if a callback returns false, the other callback will not be invoked.
return callbacks.takeIf { it.isNotEmpty() }
?.map { it.handleEventClick(timelineMode, event) }
?.all { it }
.orFalse()
}
private fun navigateToRoomMemberDetails(userId: UserId) {
callbacks.forEach { it.navigateToRoomMemberDetails(userId) }
}
private fun onLinkClick(
activity: Activity,
darkTheme: Boolean,
@ -170,7 +154,7 @@ class ThreadedMessagesNode(
is PermalinkData.UserLink -> {
// Open the room member profile, it will fallback to
// the user profile if the user is not in the room
callbacks.forEach { it.navigateToRoomMemberDetails(permalink.userId) }
callback.navigateToRoomMemberDetails(permalink.userId)
}
is PermalinkData.RoomLink -> {
handleRoomLinkClick(permalink, eventSink)
@ -204,53 +188,41 @@ class ThreadedMessagesNode(
navigateUp()
}
} else {
callbacks.forEach { it.handlePermalinkClick(roomLink) }
callback.handlePermalinkClick(roomLink)
}
}
override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
callbacks.forEach { it.navigateToEventDebugInfo(eventId, debugInfo) }
callback.navigateToEventDebugInfo(eventId, debugInfo)
}
override fun forwardEvent(eventId: EventId) {
callbacks.forEach { it.handleForwardEventClick(eventId) }
callback.handleForwardEventClick(eventId)
}
override fun navigateToReportMessage(eventId: EventId, senderId: UserId) {
callbacks.forEach { it.navigateToReportMessage(eventId, senderId) }
callback.navigateToReportMessage(eventId, senderId)
}
override fun navigateToEditPoll(eventId: EventId) {
callbacks.forEach { it.navigateToEditPoll(eventId) }
callback.navigateToEditPoll(eventId)
}
override fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) {
callbacks.forEach { it.navigateToPreviewAttachments(attachments, inReplyToEventId) }
callback.navigateToPreviewAttachments(attachments, inReplyToEventId)
}
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>) {
val permalinkData = PermalinkData.RoomLink(roomId.toRoomIdOrAlias(), eventId, viaParameters = serverNames.toImmutableList())
callbacks.forEach { it.handlePermalinkClick(permalinkData) }
callback.handlePermalinkClick(permalinkData)
}
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
callbacks.forEach { it.navigateToThread(threadRootId, focusedEventId) }
callback.navigateToThread(threadRootId, focusedEventId)
}
override fun onNavigateUp() = navigateUp()
private fun navigateToSendLocation() {
callbacks.forEach { it.navigateToSendLocation() }
}
private fun navigateToCreatePoll() {
callbacks.forEach { it.navigateToCreatePoll() }
}
private fun navigateToRoomCall() {
callbacks.forEach { it.navigateToRoomCall(room.roomId) }
}
@Composable
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
@ -271,17 +243,17 @@ class ThreadedMessagesNode(
onRoomDetailsClick = {},
onEventContentClick = { isLive, event ->
if (isLive) {
onEventClick(timelineController.mainTimelineMode(), event)
callback.handleEventClick(timelineController.mainTimelineMode(), event)
} else {
val detachedTimelineMode = timelineController.detachedTimelineMode()
if (detachedTimelineMode != null) {
onEventClick(detachedTimelineMode, event)
callback.handleEventClick(detachedTimelineMode, event)
} else {
false
}
}
},
onUserDataClick = this::navigateToRoomMemberDetails,
onUserDataClick = callback::navigateToRoomMemberDetails,
onLinkClick = { url, customTab ->
onLinkClick(
activity = activity,
@ -291,9 +263,9 @@ class ThreadedMessagesNode(
customTab = customTab,
)
},
onSendLocationClick = this::navigateToSendLocation,
onCreatePollClick = this::navigateToCreatePoll,
onJoinCallClick = this::navigateToRoomCall,
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onViewAllPinnedMessagesClick = {},
modifier = modifier,
knockRequestsBannerView = {},