From e9cfce915ad9e9b9d9cf0c2228f5fbd62fb25d40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Oct 2025 13:28:39 +0100 Subject: [PATCH 1/9] Extract code for forwarding Event to its own modules. --- features/forward/api/build.gradle.kts | 19 ++++++ .../features/forward/api/ForwardEntryPoint.kt | 36 ++++++++++ features/forward/impl/build.gradle.kts | 38 +++++++++++ .../forward/impl/DefaultForwardEntryPoint.kt | 42 ++++++++++++ .../forward/impl}/ForwardMessagesEvents.kt | 2 +- .../forward/impl}/ForwardMessagesNode.kt | 9 +-- .../forward/impl}/ForwardMessagesPresenter.kt | 4 +- .../forward/impl}/ForwardMessagesState.kt | 2 +- .../impl}/ForwardMessagesStateProvider.kt | 2 +- .../forward/impl}/ForwardMessagesView.kt | 2 +- .../impl/DefaultForwardEntryPointTest.kt | 68 +++++++++++++++++++ .../impl}/ForwardMessagesPresenterTest.kt | 26 +++---- .../forward/impl}/ForwardMessagesViewTest.kt | 2 +- features/messages/impl/build.gradle.kts | 1 + .../messages/impl/MessagesFlowNode.kt | 12 ++-- .../impl/DefaultMessagesEntryPointTest.kt | 4 ++ .../test/timeline/FakeTimelineProvider.kt | 24 +++++++ 17 files changed, 263 insertions(+), 30 deletions(-) create mode 100644 features/forward/api/build.gradle.kts create mode 100644 features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt create mode 100644 features/forward/impl/build.gradle.kts create mode 100644 features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt rename features/{messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/main/kotlin/io/element/android/features/forward/impl}/ForwardMessagesEvents.kt (83%) rename features/{messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/main/kotlin/io/element/android/features/forward/impl}/ForwardMessagesNode.kt (94%) rename features/{messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/main/kotlin/io/element/android/features/forward/impl}/ForwardMessagesPresenter.kt (97%) rename features/{messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/main/kotlin/io/element/android/features/forward/impl}/ForwardMessagesState.kt (88%) rename features/{messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/main/kotlin/io/element/android/features/forward/impl}/ForwardMessagesStateProvider.kt (95%) rename features/{messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/main/kotlin/io/element/android/features/forward/impl}/ForwardMessagesView.kt (95%) create mode 100644 features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt rename features/{messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/test/kotlin/io/element/android/features/forward/impl}/ForwardMessagesPresenterTest.kt (86%) rename features/{messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward => forward/impl/src/test/kotlin/io/element/android/features/forward/impl}/ForwardMessagesViewTest.kt (97%) create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimelineProvider.kt diff --git a/features/forward/api/build.gradle.kts b/features/forward/api/build.gradle.kts new file mode 100644 index 0000000000..b5c73539d7 --- /dev/null +++ b/features/forward/api/build.gradle.kts @@ -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.forward.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt new file mode 100644 index 0000000000..f0deaffa95 --- /dev/null +++ b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt @@ -0,0 +1,36 @@ +/* + * 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.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.timeline.TimelineProvider + +interface ForwardEntryPoint : FeatureEntryPoint { + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onForwardedToSingleRoom(roomId: RoomId) + } + + data class Params( + val eventId: EventId, + val timelineProvider: TimelineProvider, + ) : NodeInputs + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder +} diff --git a/features/forward/impl/build.gradle.kts b/features/forward/impl/build.gradle.kts new file mode 100644 index 0000000000..32da0ed7a2 --- /dev/null +++ b/features/forward/impl/build.gradle.kts @@ -0,0 +1,38 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * 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-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.forward.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupDependencyInjection() + +dependencies { + api(projects.features.forward.api) + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.roomselect.api) + + testCommonDependencies(libs, true) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.testtags) +} diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt new file mode 100644 index 0000000000..bdb10c38a3 --- /dev/null +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt @@ -0,0 +1,42 @@ +/* + * 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.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.forward.api.ForwardEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.RoomScope + +@ContributesBinding(RoomScope::class) +class DefaultForwardEntryPoint : ForwardEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ForwardEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : ForwardEntryPoint.NodeBuilder { + override fun params(params: ForwardEntryPoint.Params): ForwardEntryPoint.NodeBuilder { + plugins += ForwardMessagesNode.Inputs( + eventId = params.eventId, + timelineProvider = params.timelineProvider, + ) + return this + } + + override fun callback(callback: ForwardEntryPoint.Callback): ForwardEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt similarity index 83% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt index 05a77b61a7..06495cfc24 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl sealed interface ForwardMessagesEvents { data object ClearError : ForwardMessagesEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt index 2cf0c6e1e7..ba4e2f2cc6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import android.os.Parcelable import androidx.compose.foundation.layout.Box @@ -20,6 +20,7 @@ import com.bumble.appyx.core.plugin.Plugin import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode +import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope @@ -48,10 +49,6 @@ class ForwardMessagesNode( @Parcelize object NavTarget : Parcelable - interface Callback : Plugin { - fun onForwardedToSingleRoom(roomId: RoomId) - } - data class Inputs( val eventId: EventId, val timelineProvider: TimelineProvider, @@ -59,7 +56,7 @@ class ForwardMessagesNode( private val inputs = inputs() private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider) - private val callbacks = plugins.filterIsInstance() + private val callbacks = plugins.filterIsInstance() override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { val callback = object : RoomSelectEntryPoint.Callback { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt index 3e3860db3e..d7ffd9b398 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -36,7 +36,7 @@ class ForwardMessagesPresenter( private val eventId: EventId = EventId(eventId) @AssistedFactory - interface Factory { + fun interface Factory { fun create(eventId: String, timelineProvider: TimelineProvider): ForwardMessagesPresenter } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt similarity index 88% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt index a1de911c72..a6d93f39da 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt index b1728f4657..f5789549c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt index 9ea76d754f..214c795851 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt new file mode 100644 index 0000000000..c6bc78dbe8 --- /dev/null +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt @@ -0,0 +1,68 @@ +/* + * 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.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.forward.api.ForwardEntryPoint +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.timeline.FakeTimelineProvider +import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultForwardEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() = runTest { + val entryPoint = DefaultForwardEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + ForwardMessagesNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = { _, _ -> createForwardMessagesPresenter() }, + roomSelectEntryPoint = object : RoomSelectEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder { + lambdaError() + } + } + ) + } + val callback = object : ForwardEntryPoint.Callback { + override fun onForwardedToSingleRoom(roomId: RoomId) = lambdaError() + } + val params = ForwardEntryPoint.Params( + eventId = AN_EVENT_ID, + timelineProvider = FakeTimelineProvider(), + ) + val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) + .params(params) + .callback(callback) + .build() + assertThat(result).isInstanceOf(ForwardMessagesNode::class.java) + assertThat(result.plugins).contains( + ForwardMessagesNode.Inputs( + eventId = params.eventId, + timelineProvider = params.timelineProvider, + ) + ) + assertThat(result.plugins).contains(callback) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt similarity index 86% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt rename to features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt index 757e682592..6850a5ef07 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow @@ -32,7 +32,7 @@ class ForwardMessagesPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = aForwardMessagesPresenter() + val presenter = createForwardMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -50,7 +50,7 @@ class ForwardMessagesPresenterTest { this.forwardEventLambda = forwardEventLambda } val room = FakeJoinedRoom(liveTimeline = timeline) - val presenter = aForwardMessagesPresenter(fakeRoom = room) + val presenter = createForwardMessagesPresenter(fakeRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -74,7 +74,7 @@ class ForwardMessagesPresenterTest { this.forwardEventLambda = forwardEventLambda } val room = FakeJoinedRoom(liveTimeline = timeline) - val presenter = aForwardMessagesPresenter(fakeRoom = room) + val presenter = createForwardMessagesPresenter(fakeRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -90,13 +90,13 @@ class ForwardMessagesPresenterTest { forwardEventLambda.assertions().isCalledOnce() } } - - private fun TestScope.aForwardMessagesPresenter( - eventId: EventId = AN_EVENT_ID, - fakeRoom: FakeJoinedRoom = FakeJoinedRoom(), - ) = ForwardMessagesPresenter( - eventId = eventId.value, - timelineProvider = LiveTimelineProvider(fakeRoom), - sessionCoroutineScope = this, - ) } + +fun TestScope.createForwardMessagesPresenter( + eventId: EventId = AN_EVENT_ID, + fakeRoom: FakeJoinedRoom = FakeJoinedRoom(), +) = ForwardMessagesPresenter( + eventId = eventId.value, + timelineProvider = LiveTimelineProvider(fakeRoom), + sessionCoroutineScope = this, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt similarity index 97% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt rename to features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt index 0912fcc75c..9cecda6973 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.forward +package io.element.android.features.forward.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 43efad9da0..ee2d7f48b0 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(projects.appconfig) implementation(projects.features.call.api) implementation(projects.features.enterprise.api) + implementation(projects.features.forward.api) implementation(projects.features.location.api) implementation(projects.features.poll.api) implementation(projects.features.roomcall.api) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 82aafcf545..f08afd7d9d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -25,6 +25,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.annotations.ContributesNode import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.location.api.Location import io.element.android.features.location.api.LocationService @@ -33,7 +34,6 @@ import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode -import io.element.android.features.messages.impl.forward.ForwardMessagesNode import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode import io.element.android.features.messages.impl.report.ReportMessageNode @@ -103,6 +103,7 @@ class MessagesFlowNode( private val createPollEntryPoint: CreatePollEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint, + private val forwardEntryPoint: ForwardEntryPoint, private val analyticsService: AnalyticsService, private val locationService: LocationService, private val room: BaseRoom, @@ -333,13 +334,16 @@ class MessagesFlowNode( } else { timelineController } - val inputs = ForwardMessagesNode.Inputs(navTarget.eventId, timelineProvider) - val callback = object : ForwardMessagesNode.Callback { + val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) + val callback = object : ForwardEntryPoint.Callback { override fun onForwardedToSingleRoom(roomId: RoomId) { callbacks.forEach { it.onForwardedToSingleRoom(roomId) } } } - createNode(buildContext, listOf(inputs, callback)) + forwardEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(callback) + .build() } is NavTarget.ReportMessage -> { val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt index 2de761ad69..fed52f7840 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt @@ -15,6 +15,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint +import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint @@ -90,6 +91,9 @@ class DefaultMessagesEntryPointTest { mediaViewerEntryPoint = object : MediaViewerEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() }, + forwardEntryPoint = object : ForwardEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() + }, analyticsService = FakeAnalyticsService(), locationService = FakeLocationService(), room = FakeBaseRoom(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimelineProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimelineProvider.kt new file mode 100644 index 0000000000..519789c003 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimelineProvider.kt @@ -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.libraries.matrix.test.timeline + +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.TimelineProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTimelineProvider( + initialTimeline: Timeline? = null, +) : TimelineProvider { + private val timelineFlow = MutableStateFlow(initialTimeline) + + override fun activeTimelineFlow(): StateFlow { + return timelineFlow.asStateFlow() + } +} From 21bae4aee2c0a6b9656dfb59a1d3b27eaf9fcfe2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Oct 2025 16:02:37 +0100 Subject: [PATCH 2/9] Add Forward action to MediaDetailsBottomSheet. Closes #5454 Improve API of Callback when forwarding Event. --- .../android/appnav/LoggedInFlowNode.kt | 4 -- .../room/joined/JoinedRoomLoadedFlowNode.kt | 37 ++++++++++++++++--- .../appnav/JoinedRoomLoadedFlowNodeTest.kt | 13 +++++++ .../features/forward/api/ForwardEntryPoint.kt | 2 +- .../forward/impl/DefaultForwardEntryPoint.kt | 4 +- .../forward/impl/ForwardMessagesNode.kt | 16 +++----- .../impl/DefaultForwardEntryPointTest.kt | 2 +- .../messages/api/MessagesEntryPoint.kt | 3 +- .../messages/impl/MessagesFlowNode.kt | 22 +++++++++-- .../impl/DefaultMessagesEntryPointTest.kt | 3 +- .../roomdetails/api/RoomDetailsEntryPoint.kt | 3 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 16 +++++++- .../impl/DefaultRoomDetailsEntryPointTest.kt | 2 +- .../userprofile/impl/UserProfileFlowNode.kt | 4 ++ .../matrix/api/timeline/TimelineProvider.kt | 2 +- .../mediaviewer/api/MediaGalleryEntryPoint.kt | 1 + .../mediaviewer/api/MediaViewerEntryPoint.kt | 1 + libraries/mediaviewer/impl/build.gradle.kts | 1 + .../impl/details/MediaDetailsBottomSheet.kt | 10 +++++ .../impl/gallery/MediaGalleryEvents.kt | 1 + .../impl/gallery/MediaGalleryNavigator.kt | 1 + .../impl/gallery/MediaGalleryNode.kt | 7 ++++ .../impl/gallery/MediaGalleryPresenter.kt | 4 ++ .../impl/gallery/MediaGalleryView.kt | 3 ++ .../impl/gallery/root/MediaGalleryFlowNode.kt | 17 ++++++++- .../impl/viewer/MediaViewerEvents.kt | 1 + .../impl/viewer/MediaViewerNavigator.kt | 1 + .../impl/viewer/MediaViewerNode.kt | 6 +++ .../impl/viewer/MediaViewerPresenter.kt | 4 ++ .../impl/viewer/MediaViewerView.kt | 3 ++ .../impl/DefaultMediaGalleryEntryPointTest.kt | 3 +- .../impl/DefaultMediaViewerEntryPointTest.kt | 2 + .../details/MediaDetailsBottomSheetTest.kt | 15 ++++++++ .../impl/gallery/FakeMediaGalleryNavigator.kt | 7 +++- .../impl/viewer/FakeMediaViewerNavigator.kt | 5 +++ 35 files changed, 190 insertions(+), 36 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index eb1df5643e..22328d3980 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -342,10 +342,6 @@ class LoggedInFlowNode( backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames)) } - override fun onForwardedToSingleRoom(roomId: RoomId) { - sessionCoroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false) } - } - override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) { when (data) { is PermalinkData.UserLink -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 93e0c64a7f..f7140804c8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -16,6 +16,7 @@ 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.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject @@ -24,6 +25,7 @@ import io.element.android.appnav.di.RoomGraphFactory import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode.Inputs import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode.NavTarget +import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.space.api.SpaceEntryPoint @@ -43,6 +45,8 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -55,6 +59,7 @@ class JoinedRoomLoadedFlowNode( private val messagesEntryPoint: MessagesEntryPoint, private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val spaceEntryPoint: SpaceEntryPoint, + private val forwardEntryPoint: ForwardEntryPoint, private val appNavigationStateService: AppNavigationStateService, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @@ -72,7 +77,6 @@ class JoinedRoomLoadedFlowNode( interface Callback : Plugin { fun onOpenRoom(roomId: RoomId, serverNames: List) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) - fun onForwardedToSingleRoom(roomId: RoomId) fun onOpenGlobalNotificationSettings() } @@ -130,8 +134,8 @@ class JoinedRoomLoadedFlowNode( callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } } - override fun onForwardedToSingleRoom(roomId: RoomId) { - callbacks.forEach { it.onForwardedToSingleRoom(roomId) } + override fun forwardEvent(eventId: EventId) { + backstack.push(NavTarget.ForwardEvent(eventId)) } } return roomDetailsEntryPoint.nodeBuilder(this, buildContext) @@ -157,6 +161,22 @@ class JoinedRoomLoadedFlowNode( NavTarget.Space -> { createSpaceNode(buildContext) } + is NavTarget.ForwardEvent -> { + val timelineProvider = { MutableStateFlow(inputs.room.liveTimeline).asStateFlow() } + val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) + val callback = object : ForwardEntryPoint.Callback { + override fun onForwardDone(roomIds: List) { + backstack.pop() + roomIds.singleOrNull()?.let { roomId -> + callbacks.forEach { it.onOpenRoom(roomId, emptyList()) } + } + } + } + forwardEntryPoint.nodeBuilder(this, buildContext) + .params(params) + .callback(callback) + .build() + } } } @@ -193,8 +213,12 @@ class JoinedRoomLoadedFlowNode( callbacks.forEach { it.onPermalinkClick(data, pushToBackstack) } } - override fun onForwardedToSingleRoom(roomId: RoomId) { - callbacks.forEach { it.onForwardedToSingleRoom(roomId) } + override fun forwardEvent(eventId: EventId) { + backstack.push(NavTarget.ForwardEvent(eventId)) + } + + override fun openRoom(roomId: RoomId) { + callbacks.forEach { it.onOpenRoom(roomId, emptyList()) } } } val params = MessagesEntryPoint.Params( @@ -219,6 +243,9 @@ class JoinedRoomLoadedFlowNode( @Parcelize data class RoomMemberDetails(val userId: UserId) : NavTarget + @Parcelize + data class ForwardEvent(val eventId: EventId) : NavTarget + @Parcelize data object RoomNotificationSettings : NavTarget } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt index f8a18fa24a..8e73165cb6 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.appnav.di.RoomGraphFactory import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode +import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.space.api.SpaceEntryPoint @@ -122,11 +123,22 @@ class JoinedRoomLoadedFlowNodeTest { } } + 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) {} + } + } + } + private fun TestScope.createJoinedRoomLoadedFlowNode( plugins: List, messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(), + forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(), activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), ) = JoinedRoomLoadedFlowNode( buildContext = BuildContext.root(savedStateMap = null), @@ -134,6 +146,7 @@ class JoinedRoomLoadedFlowNodeTest { messagesEntryPoint = messagesEntryPoint, roomDetailsEntryPoint = roomDetailsEntryPoint, spaceEntryPoint = spaceEntryPoint, + forwardEntryPoint = forwardEntryPoint, appNavigationStateService = FakeAppNavigationStateService(), sessionCoroutineScope = this, roomGraphFactory = FakeRoomGraphFactory(), diff --git a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt index f0deaffa95..e0632ca20c 100644 --- a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt +++ b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt @@ -24,7 +24,7 @@ interface ForwardEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onForwardedToSingleRoom(roomId: RoomId) + fun onForwardDone(roomIds: List) } data class Params( diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt index bdb10c38a3..55eede7b57 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt @@ -13,9 +13,9 @@ import com.bumble.appyx.core.plugin.Plugin import dev.zacsweers.metro.ContributesBinding import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SessionScope -@ContributesBinding(RoomScope::class) +@ContributesBinding(SessionScope::class) class DefaultForwardEntryPoint : ForwardEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ForwardEntryPoint.NodeBuilder { val plugins = ArrayList() diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt index ba4e2f2cc6..3bab03ff65 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt @@ -23,7 +23,7 @@ import io.element.android.annotations.ContributesNode import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.RoomScope +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 import io.element.android.libraries.matrix.api.timeline.TimelineProvider @@ -31,7 +31,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode import kotlinx.parcelize.Parcelize -@ContributesNode(RoomScope::class) +@ContributesNode(SessionScope::class) @AssistedInject class ForwardMessagesNode( @Assisted buildContext: BuildContext, @@ -65,7 +65,7 @@ class ForwardMessagesNode( } override fun onCancel() { - navigateUp() + onForwardDone(emptyList()) } } @@ -86,16 +86,12 @@ class ForwardMessagesNode( val state = presenter.present() ForwardMessagesView( state = state, - onForwardSuccess = ::onForwardSuccess, + onForwardSuccess = ::onForwardDone, ) } } - private fun onForwardSuccess(roomIds: List) { - navigateUp() - if (roomIds.size == 1) { - val targetRoomId = roomIds.first() - callbacks.forEach { it.onForwardedToSingleRoom(targetRoomId) } - } + private fun onForwardDone(roomIds: List) { + callbacks.forEach { it.onForwardDone(roomIds) } } } diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt index c6bc78dbe8..9af564f50d 100644 --- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt @@ -46,7 +46,7 @@ class DefaultForwardEntryPointTest { ) } val callback = object : ForwardEntryPoint.Callback { - override fun onForwardedToSingleRoom(roomId: RoomId) = lambdaError() + override fun onForwardDone(roomIds: List) = lambdaError() } val params = ForwardEntryPoint.Params( eventId = AN_EVENT_ID, diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 706e292dbb..8478a57940 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -38,7 +38,8 @@ interface MessagesEntryPoint : FeatureEntryPoint { fun onRoomDetailsClick() fun onUserDataClick(userId: UserId) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) - fun onForwardedToSingleRoom(roomId: RoomId) + fun forwardEvent(eventId: EventId) + fun openRoom(roomId: RoomId) } data class Params(val initialTarget: InitialTarget) : NodeInputs diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index f08afd7d9d..e9cec2b652 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -18,6 +18,7 @@ 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 import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject @@ -150,7 +151,10 @@ class MessagesFlowNode( data class EventDebugInfo(val eventId: EventId?, val debugInfo: TimelineItemDebugInfo) : NavTarget @Parcelize - data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget + data class ForwardEvent( + val eventId: EventId, + val fromPinnedEvents: Boolean, + ) : NavTarget @Parcelize data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget @@ -306,6 +310,11 @@ class MessagesFlowNode( override fun onViewInTimeline(eventId: EventId) { viewInTimeline(eventId) } + + override fun onForwardEvent(eventId: EventId) { + // Need to go to the parent because of the overlay + forwardEvent(eventId) + } } mediaViewerEntryPoint.nodeBuilder(this, buildContext) .params(params) @@ -336,8 +345,11 @@ class MessagesFlowNode( } val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) val callback = object : ForwardEntryPoint.Callback { - override fun onForwardedToSingleRoom(roomId: RoomId) { - callbacks.forEach { it.onForwardedToSingleRoom(roomId) } + override fun onForwardDone(roomIds: List) { + backstack.pop() + roomIds.singleOrNull()?.let { roomId -> + callbacks.forEach { it.openRoom(roomId) } + } } } forwardEntryPoint.nodeBuilder(this, buildContext) @@ -489,6 +501,10 @@ class MessagesFlowNode( callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) } } + private fun forwardEvent(eventId: EventId) { + callbacks.forEach { it.forwardEvent(eventId) } + } + private fun processEventClick( timelineMode: Timeline.Mode, event: TimelineItem.Event, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt index fed52f7840..dc7e5ad5d4 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt @@ -119,7 +119,8 @@ class DefaultMessagesEntryPointTest { override fun onRoomDetailsClick() = lambdaError() override fun onUserDataClick(userId: UserId) = lambdaError() override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() - override fun onForwardedToSingleRoom(roomId: RoomId) = lambdaError() + override fun forwardEvent(eventId: EventId) = lambdaError() + override fun openRoom(roomId: RoomId) = lambdaError() } val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID) val params = MessagesEntryPoint.Params(initialTarget) diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 803002d642..d68cb0f5e8 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData @@ -36,7 +37,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { fun onOpenGlobalNotificationSettings() fun onOpenRoom(roomId: RoomId, serverNames: List) fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) - fun onForwardedToSingleRoom(roomId: RoomId) + fun forwardEvent(eventId: EventId) } interface NodeBuilder { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 2b4bf10d67..a72f0394b4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -294,6 +294,10 @@ class RoomDetailsFlowNode( override fun onViewInTimeline(eventId: EventId) { // Cannot happen } + + override fun onForwardEvent(eventId: EventId) { + // Cannot happen + } } mediaViewerEntryPoint.nodeBuilder(this, buildContext) .avatar( @@ -321,6 +325,10 @@ class RoomDetailsFlowNode( it.onPermalinkClick(permalinkData, pushToBackstack = false) } } + + override fun forwardEvent(eventId: EventId) { + plugins().forEach { it.forwardEvent(eventId) } + } } mediaGalleryEntryPoint.nodeBuilder(this, buildContext) .callback(callback) @@ -343,8 +351,12 @@ class RoomDetailsFlowNode( plugins().forEach { it.onPermalinkClick(data, pushToBackstack) } } - override fun onForwardedToSingleRoom(roomId: RoomId) { - plugins().forEach { it.onForwardedToSingleRoom(roomId) } + override fun forwardEvent(eventId: EventId) { + plugins().forEach { it.forwardEvent(eventId) } + } + + override fun openRoom(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId, emptyList()) } } } return messagesEntryPoint.nodeBuilder(this, buildContext) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt index f2412e616b..a9d528ace5 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt @@ -97,7 +97,7 @@ class DefaultRoomDetailsEntryPointTest { override fun onOpenGlobalNotificationSettings() = lambdaError() override fun onOpenRoom(roomId: RoomId, serverNames: List) = lambdaError() override fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() - override fun onForwardedToSingleRoom(roomId: RoomId) = lambdaError() + override fun forwardEvent(eventId: EventId) = lambdaError() } val params = RoomDetailsEntryPoint.Params( initialElement = RoomDetailsEntryPoint.InitialTarget.RoomDetails, diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index 5828d60c25..da3adef973 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -101,6 +101,10 @@ class UserProfileFlowNode( override fun onViewInTimeline(eventId: EventId) { // Cannot happen } + + override fun onForwardEvent(eventId: EventId) { + // Cannot happen + } } mediaViewerEntryPoint.nodeBuilder(this, buildContext) .avatar( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt index acc18ee1a0..592b5ab3ba 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.first * It could be the live timeline, a pinned timeline or a detached timeline. * By default, the active timeline is the live timeline. */ -interface TimelineProvider { +fun interface TimelineProvider { fun activeTimelineFlow(): StateFlow } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt index 841e82f195..119c1002c8 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt @@ -24,5 +24,6 @@ interface MediaGalleryEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onBackClick() fun onViewInTimeline(eventId: EventId) + fun forwardEvent(eventId: EventId) } } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt index 906283c457..b16de69b98 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt @@ -31,6 +31,7 @@ interface MediaViewerEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onDone() fun onViewInTimeline(eventId: EventId) + fun onForwardEvent(eventId: EventId) } data class Params( diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index ea435433f4..eb49387372 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(libs.telephoto.flick) implementation(projects.features.enterprise.api) + implementation(projects.features.forward.api) implementation(projects.features.viewfolder.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt index 22c29c590b..35d5a3e518 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt @@ -49,6 +49,7 @@ fun MediaDetailsBottomSheet( state: MediaBottomSheetState.MediaDetailsBottomSheetState, onViewInTimeline: (EventId) -> Unit, onShare: (EventId) -> Unit, + onForward: (EventId) -> Unit, onDownload: (EventId) -> Unit, onDelete: (EventId) -> Unit, onDismiss: () -> Unit, @@ -102,6 +103,14 @@ fun MediaDetailsBottomSheet( onShare(state.eventId) } ) + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Forward())), + headlineContent = { Text(stringResource(CommonStrings.action_forward)) }, + style = ListItemStyle.Primary, + onClick = { + onForward(state.eventId) + } + ) ListItem( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())), headlineContent = { Text(stringResource(CommonStrings.action_save)) }, @@ -216,6 +225,7 @@ internal fun MediaDetailsBottomSheetPreview() = ElementPreview { state = aMediaDetailsBottomSheetState(), onViewInTimeline = {}, onShare = {}, + onForward = {}, onDownload = {}, onDelete = {}, onDismiss = {}, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt index df7d82c7b2..f7a03020c5 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt @@ -17,6 +17,7 @@ sealed interface MediaGalleryEvents { data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents data class Share(val eventId: EventId?) : MediaGalleryEvents + data class Forward(val eventId: EventId) : MediaGalleryEvents data class SaveOnDisk(val eventId: EventId?) : MediaGalleryEvents data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt index 0195e7f39c..8ce4860c53 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt @@ -11,4 +11,5 @@ import io.element.android.libraries.matrix.api.core.EventId interface MediaGalleryNavigator { fun onViewInTimelineClick(eventId: EventId) + fun onForwardClick(eventId: EventId) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt index 06a3c6a58f..6ee31c6520 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt @@ -40,6 +40,7 @@ class MediaGalleryNode( fun onBackClick() fun onItemClick(item: MediaItem.Event) fun onViewInTimeline(eventId: EventId) + fun onForward(eventId: EventId) } private fun onBackClick() { @@ -54,6 +55,12 @@ class MediaGalleryNode( } } + override fun onForwardClick(eventId: EventId) { + plugins().forEach { + it.onForward(eventId) + } + } + private fun onItemClick(item: MediaItem.Event) { plugins().forEach { it.onItemClick(item) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index ba3d4d5e1f..3f8a31cc26 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -105,6 +105,10 @@ class MediaGalleryPresenter( share(it) } } + is MediaGalleryEvents.Forward -> { + mediaBottomSheetState = MediaBottomSheetState.Hidden + navigator.onForwardClick(event.eventId) + } is MediaGalleryEvents.ViewInTimeline -> { mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onViewInTimelineClick(event.eventId) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt index a48ff93d71..50a7fe9aad 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt @@ -166,6 +166,9 @@ fun MediaGalleryView( onShare = { eventId -> state.eventSink(MediaGalleryEvents.Share(eventId)) }, + onForward = { eventId -> + state.eventSink(MediaGalleryEvents.Forward(eventId)) + }, onDownload = { eventId -> state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId)) }, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt index e617829ad0..76f025006f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt @@ -44,7 +44,7 @@ import kotlinx.parcelize.Parcelize class MediaGalleryFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val mediaViewerEntryPoint: MediaViewerEntryPoint + private val mediaViewerEntryPoint: MediaViewerEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -82,6 +82,12 @@ class MediaGalleryFlowNode( } } + private fun forwardEvent(eventId: EventId) { + plugins().forEach { + it.forwardEvent(eventId) + } + } + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { @@ -94,6 +100,10 @@ class MediaGalleryFlowNode( this@MediaGalleryFlowNode.onViewInTimeline(eventId) } + override fun onForward(eventId: EventId) { + forwardEvent(eventId) + } + override fun onItemClick(item: MediaItem.Event) { val mode = when (item) { is MediaItem.Audio, @@ -124,6 +134,11 @@ class MediaGalleryFlowNode( override fun onViewInTimeline(eventId: EventId) { this@MediaGalleryFlowNode.onViewInTimeline(eventId) } + + override fun onForwardEvent(eventId: EventId) { + // Need to go to the parent because of the overlay + forwardEvent(eventId) + } } mediaViewerEntryPoint.nodeBuilder(this, buildContext) .params( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt index 708c423d36..519100b610 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt @@ -17,6 +17,7 @@ sealed interface MediaViewerEvents { data class OpenWith(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class ClearLoadingError(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class ViewInTimeline(val eventId: EventId) : MediaViewerEvents + data class Forward(val eventId: EventId) : MediaViewerEvents data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class ConfirmDelete( val eventId: EventId, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt index c75db00afe..77e253dfa5 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt @@ -11,5 +11,6 @@ import io.element.android.libraries.matrix.api.core.EventId interface MediaViewerNavigator { fun onViewInTimelineClick(eventId: EventId) + fun onForwardClick(eventId: EventId) fun onItemDeleted() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 6599411411..ee874156cd 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -71,6 +71,12 @@ class MediaViewerNode( } } + override fun onForwardClick(eventId: EventId) { + plugins().forEach { + it.onForwardEvent(eventId) + } + } + override fun onItemDeleted() { onDone() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index c7b93a227f..726e9989ce 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -117,6 +117,10 @@ class MediaViewerPresenter( mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onViewInTimelineClick(event.eventId) } + is MediaViewerEvents.Forward -> { + mediaBottomSheetState = MediaBottomSheetState.Hidden + navigator.onForwardClick(event.eventId) + } is MediaViewerEvents.OpenInfo -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState( eventId = event.data.eventId, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index 110054eb20..791bd1bc8e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -247,6 +247,9 @@ fun MediaViewerView( state.eventSink(MediaViewerEvents.Share(currentData)) } }, + onForward = { + state.eventSink(MediaViewerEvents.Forward(it)) + }, onDownload = { (currentData as? MediaViewerPageData.MediaViewerData)?.let { state.eventSink(MediaViewerEvents.SaveOnDisk(currentData)) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt index 0f8b0fedfb..38040426e1 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt @@ -37,12 +37,13 @@ class DefaultMediaGalleryEntryPointTest { plugins = plugins, mediaViewerEntryPoint = object : MediaViewerEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError() - } + }, ) } val callback = object : MediaGalleryEntryPoint.Callback { override fun onBackClick() = lambdaError() override fun onViewInTimeline(eventId: EventId) = lambdaError() + override fun forwardEvent(eventId: EventId) = lambdaError() } val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) .callback(callback) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt index ddc04fc6ce..a1f60cc124 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt @@ -72,6 +72,7 @@ class DefaultMediaViewerEntryPointTest { val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() = lambdaError() override fun onViewInTimeline(eventId: EventId) = lambdaError() + override fun onForwardEvent(eventId: EventId) = lambdaError() } val params = createMediaViewerEntryPointParams() val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) @@ -115,6 +116,7 @@ class DefaultMediaViewerEntryPointTest { val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() = lambdaError() override fun onViewInTimeline(eventId: EventId) = lambdaError() + override fun onForwardEvent(eventId: EventId) = lambdaError() } val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) .avatar( diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index bd4373a420..3177ed0774 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -56,6 +56,19 @@ class MediaDetailsBottomSheetTest { } } + @Test + @Config(qualifiers = "h1024dp") + fun `clicking on Forward invokes expected callback`() { + val state = aMediaDetailsBottomSheetState() + ensureCalledOnceWithParam(state.eventId) { callback -> + rule.setMediaDetailsBottomSheet( + state = state, + onForward = callback, + ) + rule.clickOn(CommonStrings.action_forward) + } + } + @Test @Config(qualifiers = "h1024dp") fun `clicking on Save invokes expected callback`() { @@ -100,6 +113,7 @@ private fun AndroidComposeTestRule.setMedia state: MediaBottomSheetState.MediaDetailsBottomSheetState, onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(), onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(), + onForward: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), @@ -109,6 +123,7 @@ private fun AndroidComposeTestRule.setMedia state = state, onViewInTimeline = onViewInTimeline, onShare = onShare, + onForward = onForward, onDownload = onDownload, onDelete = onDelete, onDismiss = onDismiss, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt index 2566ef91a4..8727335e6d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt @@ -11,9 +11,14 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.tests.testutils.lambda.lambdaError class FakeMediaGalleryNavigator( - private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() } + private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() }, + private val onForwardClickLambda: (EventId) -> Unit = { lambdaError() }, ) : MediaGalleryNavigator { override fun onViewInTimelineClick(eventId: EventId) { onViewInTimelineClickLambda(eventId) } + + override fun onForwardClick(eventId: EventId) { + onForwardClickLambda(eventId) + } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt index 6c0148b124..791527445b 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt @@ -12,12 +12,17 @@ import io.element.android.tests.testutils.lambda.lambdaError class FakeMediaViewerNavigator( private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() }, + private val onForwardClickLambda: (EventId) -> Unit = { lambdaError() }, private val onItemDeletedLambda: () -> Unit = { lambdaError() }, ) : MediaViewerNavigator { override fun onViewInTimelineClick(eventId: EventId) { onViewInTimelineClickLambda(eventId) } + override fun onForwardClick(eventId: EventId) { + onForwardClickLambda(eventId) + } + override fun onItemDeleted() { onItemDeletedLambda() } From 700362a26642b2ed7d1d8b5bc69d820d66584ecc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Oct 2025 18:15:25 +0100 Subject: [PATCH 3/9] EventId cannot be null here. --- .../libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt index f7a03020c5..1a5bf70170 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt @@ -16,9 +16,9 @@ import io.element.android.libraries.mediaviewer.impl.model.MediaItem sealed interface MediaGalleryEvents { data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents - data class Share(val eventId: EventId?) : MediaGalleryEvents + data class Share(val eventId: EventId) : MediaGalleryEvents data class Forward(val eventId: EventId) : MediaGalleryEvents - data class SaveOnDisk(val eventId: EventId?) : MediaGalleryEvents + data class SaveOnDisk(val eventId: EventId) : MediaGalleryEvents data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents From 2fe3bfb3bca9871401f94f61dac07c018198de10 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Oct 2025 18:21:06 +0100 Subject: [PATCH 4/9] Simplify the presenter --- .../features/forward/impl/ForwardMessagesPresenter.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt index d7ffd9b398..63fbb0bc7c 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt @@ -21,8 +21,6 @@ import io.element.android.libraries.matrix.api.core.EventId 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.getActiveTimeline -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -43,7 +41,7 @@ class ForwardMessagesPresenter( private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized) fun onRoomSelected(roomIds: List) { - sessionCoroutineScope.forwardEvent(eventId, roomIds.toImmutableList(), forwardingActionState) + sessionCoroutineScope.forwardEvent(eventId, roomIds) } @Composable @@ -62,12 +60,11 @@ class ForwardMessagesPresenter( private fun CoroutineScope.forwardEvent( eventId: EventId, - roomIds: ImmutableList, - isForwardMessagesState: MutableState>>, + roomIds: List, ) = launch { suspend { timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).getOrThrow() roomIds - }.runCatchingUpdatingState(isForwardMessagesState) + }.runCatchingUpdatingState(forwardingActionState) } } From 14ff37ec8388e3efc239577b2e62ce1719e5b201 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 28 Oct 2025 17:47:27 +0000 Subject: [PATCH 5/9] Update screenshots --- ...=> features.forward.impl_ForwardMessagesView_Day_0_en.png} | 0 ...=> features.forward.impl_ForwardMessagesView_Day_1_en.png} | 0 ...=> features.forward.impl_ForwardMessagesView_Day_2_en.png} | 0 ...=> features.forward.impl_ForwardMessagesView_Day_3_en.png} | 0 ... features.forward.impl_ForwardMessagesView_Night_0_en.png} | 0 ... features.forward.impl_ForwardMessagesView_Night_1_en.png} | 0 ... features.forward.impl_ForwardMessagesView_Night_2_en.png} | 0 ... features.forward.impl_ForwardMessagesView_Night_3_en.png} | 0 ...iaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png | 4 ++-- ...viewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png | 4 ++-- ...ies.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png | 4 ++-- ...s.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png | 4 ++-- ...ibraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png | 4 ++-- 13 files changed, 10 insertions(+), 10 deletions(-) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Day_0_en.png => features.forward.impl_ForwardMessagesView_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Day_1_en.png => features.forward.impl_ForwardMessagesView_Day_1_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Day_2_en.png => features.forward.impl_ForwardMessagesView_Day_2_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Day_3_en.png => features.forward.impl_ForwardMessagesView_Day_3_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Night_0_en.png => features.forward.impl_ForwardMessagesView_Night_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Night_1_en.png => features.forward.impl_ForwardMessagesView_Night_1_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Night_2_en.png => features.forward.impl_ForwardMessagesView_Night_2_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.messages.impl.forward_ForwardMessagesView_Night_3_en.png => features.forward.impl_ForwardMessagesView_Night_3_en.png} (100%) diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_1_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_2_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Day_3_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_1_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_2_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_2_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_2_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_3_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.messages.impl.forward_ForwardMessagesView_Night_3_en.png rename to tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png index 2ecb6f0e65..9ebcafa2e0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:964bac6ba83961f303f2309438a81fb4a09a87a0bc269df1f644b97b52044192 -size 38109 +oid sha256:81bdd5170870d3ea5874960b12d9548c36b105a95dcb018f648182b9206f17d6 +size 40196 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png index ebed9fbaea..03f88988e5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e423c47cee8f2c69b31d418a7a8db850542ee2b0040c99775c85a4260b286235 -size 36787 +oid sha256:51594e2535dbe22f9085ff3443fa89ab7fc39c22beab5f592b1a4953bab3a1c9 +size 38931 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png index a2ec7507ed..2f643f7ffd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:090412a4d7df8699d54070a04f5514580d46adc497cdbc1b354631506eeeda3b -size 40872 +oid sha256:0309be2e3e391a852cb00d5490c92b45255b64d088fd1ae8a5b8472a47ce9f88 +size 40129 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png index bf10c81b0b..9248804599 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7bba63c8d4018b9bba182e78397e94e85040f8a50596d1c528895278e219ea1 -size 39333 +oid sha256:add239fbe0f1047435c7ad5df8b62ba765098eb0c5b9703192a7276b6c4e0ccc +size 38692 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png index b094052a00..df4212be1e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f47067ff716b6ada901650d138a7f1f88e1f1cb29d9a919d4d5af5d834b571ab -size 38045 +oid sha256:d004bb0bb2e6dec6e5bc28038cfb2bddbd68f0c430b9bf3bc1c08de1d90a3301 +size 38738 From f5b17d4ddb78788edc592dc913a8aecbd7def011 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Oct 2025 18:40:09 +0100 Subject: [PATCH 6/9] Remove unused dependency --- libraries/mediaviewer/impl/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index eb49387372..ea435433f4 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -33,7 +33,6 @@ dependencies { implementation(libs.telephoto.flick) implementation(projects.features.enterprise.api) - implementation(projects.features.forward.api) implementation(projects.features.viewfolder.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) From 6ae0d67e69992a4a6344194f4c3774a714fb2eb5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Oct 2025 20:28:37 +0100 Subject: [PATCH 7/9] Add missing tests. --- .../impl/gallery/MediaGalleryPresenterTest.kt | 47 ++++++++++++++++++- .../impl/viewer/MediaViewerPresenterTest.kt | 27 ++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index f126dab2b5..b09aae5d26 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -345,7 +345,7 @@ class MediaGalleryPresenterTest { } @Test - fun `present - view in timeline invokes the navigator`() = runTest { + fun `present - view in timeline closes the bottom sheet and invokes the navigator`() = runTest { val onViewInTimelineClickLambda = lambdaRecorder { } val navigator = FakeMediaGalleryNavigator( onViewInTimelineClickLambda = onViewInTimelineClickLambda, @@ -353,16 +353,59 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter( room = FakeJoinedRoom( createTimelineResult = { Result.success(FakeTimeline()) }, + baseRoom = FakeBaseRoom( + canRedactOwnResult = { Result.success(true) }, + ), ), navigator = navigator, ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID)) + val item = aMediaItemImage( + eventId = AN_EVENT_ID, + senderId = A_USER_ID, + ) + initialState.eventSink(MediaGalleryEvents.OpenInfo(item)) + val withBottomSheetState = awaitItem() + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + withBottomSheetState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) } } + @Test + fun `present - forward closes the bottom sheet and invokes the navigator`() = runTest { + val onForwardClickLambda = lambdaRecorder { } + val navigator = FakeMediaGalleryNavigator( + onForwardClickLambda = onForwardClickLambda, + ) + val presenter = createMediaGalleryPresenter( + room = FakeJoinedRoom( + createTimelineResult = { Result.success(FakeTimeline()) }, + baseRoom = FakeBaseRoom( + canRedactOwnResult = { Result.success(true) }, + ), + ), + navigator = navigator, + ) + presenter.test { + val initialState = awaitFirstItem() + val item = aMediaItemImage( + eventId = AN_EVENT_ID, + senderId = A_USER_ID, + ) + initialState.eventSink(MediaGalleryEvents.OpenInfo(item)) + val withBottomSheetState = awaitItem() + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + withBottomSheetState.eventSink(MediaGalleryEvents.Forward(AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + onForwardClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + } + } + @Test fun `present - load more`() = runTest { val loadMoreLambda = lambdaRecorder { } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index b9964d8d3d..359eda7a5e 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -759,7 +759,7 @@ class MediaViewerPresenterTest { } @Test - fun `present - view in timeline hide the bottom sheet and invokes the navigator`() = runTest { + fun `present - view in timeline hides the bottom sheet and invokes the navigator`() = runTest { val onViewInTimelineClickLambda = lambdaRecorder { } val navigator = FakeMediaViewerNavigator( onViewInTimelineClickLambda = onViewInTimelineClickLambda, @@ -783,6 +783,31 @@ class MediaViewerPresenterTest { } } + @Test + fun `present - forward hides the bottom sheet and invokes the navigator`() = runTest { + val onForwardClickLambda = lambdaRecorder { } + val navigator = FakeMediaViewerNavigator( + onForwardClickLambda = onForwardClickLambda, + ) + val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, + mediaViewerNavigator = navigator, + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData())) + val withBottomSheetState = awaitItem() + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + onForwardClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } From 93b1c9e597b0d66cd1c98285bbc186d2209e9033 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Oct 2025 09:00:07 +0100 Subject: [PATCH 8/9] Improve code on ShareNode --- .../kotlin/io/element/android/appnav/LoggedInFlowNode.kt | 5 ++--- .../io/element/android/features/share/impl/ShareNode.kt | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 22328d3980..da09add2dc 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -469,9 +469,8 @@ class LoggedInFlowNode( .callback(object : ShareEntryPoint.Callback { override fun onDone(roomIds: List) { navigateUp() - if (roomIds.size == 1) { - val targetRoomId = roomIds.first() - backstack.push(NavTarget.Room(targetRoomId.toRoomIdOrAlias())) + roomIds.singleOrNull()?.let { roomId -> + backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) } } }) diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt index e268419920..12d2f5bff5 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt @@ -61,7 +61,7 @@ class ShareNode( } override fun onCancel() { - navigateUp() + onShareDone(emptyList()) } } @@ -82,12 +82,12 @@ class ShareNode( val state = presenter.present() ShareView( state = state, - onShareSuccess = ::onShareSuccess, + onShareSuccess = ::onShareDone, ) } } - private fun onShareSuccess(roomIds: List) { + private fun onShareDone(roomIds: List) { callbacks.forEach { it.onDone(roomIds) } } } From 725e6c9855d7bba9200490e24db64d9233f9894a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Oct 2025 09:04:58 +0100 Subject: [PATCH 9/9] Rename fun. --- .../android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt | 2 +- .../element/android/features/forward/api/ForwardEntryPoint.kt | 2 +- .../android/features/forward/impl/ForwardMessagesNode.kt | 2 +- .../features/forward/impl/DefaultForwardEntryPointTest.kt | 2 +- .../element/android/features/messages/impl/MessagesFlowNode.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index f7140804c8..f09113afa0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -165,7 +165,7 @@ class JoinedRoomLoadedFlowNode( val timelineProvider = { MutableStateFlow(inputs.room.liveTimeline).asStateFlow() } val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) val callback = object : ForwardEntryPoint.Callback { - override fun onForwardDone(roomIds: List) { + override fun onDone(roomIds: List) { backstack.pop() roomIds.singleOrNull()?.let { roomId -> callbacks.forEach { it.onOpenRoom(roomId, emptyList()) } diff --git a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt index e0632ca20c..d822c0adb8 100644 --- a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt +++ b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt @@ -24,7 +24,7 @@ interface ForwardEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onForwardDone(roomIds: List) + fun onDone(roomIds: List) } data class Params( diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt index 3bab03ff65..27e95898d2 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt @@ -92,6 +92,6 @@ class ForwardMessagesNode( } private fun onForwardDone(roomIds: List) { - callbacks.forEach { it.onForwardDone(roomIds) } + callbacks.forEach { it.onDone(roomIds) } } } diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt index 9af564f50d..dabb8af4a1 100644 --- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt @@ -46,7 +46,7 @@ class DefaultForwardEntryPointTest { ) } val callback = object : ForwardEntryPoint.Callback { - override fun onForwardDone(roomIds: List) = lambdaError() + override fun onDone(roomIds: List) = lambdaError() } val params = ForwardEntryPoint.Params( eventId = AN_EVENT_ID, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index e9cec2b652..8807dd77cb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -345,7 +345,7 @@ class MessagesFlowNode( } val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) val callback = object : ForwardEntryPoint.Callback { - override fun onForwardDone(roomIds: List) { + override fun onDone(roomIds: List) { backstack.pop() roomIds.singleOrNull()?.let { roomId -> callbacks.forEach { it.openRoom(roomId) }