Repository: element-hq/element-x-android (language: kotlin)

Issue:
[Task] Add the Forward option to the info sheet in Media and Files
Please see and discuss the details in the meta issue.

[Figma](https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline--new-?node-id=1732-80010&t=6BlabuIHijm3tqKP-0)

This sheet is displayed in 2 places
- [x] Gallery
- [x] Media full screen



Discussion hints:
nan

Pull request: Add ability to forward a media from the media viewer and the gallery
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Content

<!-- Describe shortly what has been changed -->
Add ability to forward a media from the media viewer and the gallery.

First commit extract the forward Node to its own module so that it can be used by other modules in particular `JoinedRoomLoadedFlowNode`. To be able to push a Node to the backstack above the Overlay of the media viewer wew could not use the backstack of the media gallery / the messages node.

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #5454

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

### Case 1

- From a timelline, click on an image
- click on info (top end icon)
- observe a new "Forward" action
- try it, cancel it, etc.

### Case 2

- open the room settings from the room list
- open the media gallery
- long click on an item
- observe a new "Forward" action
- try it, cancel it, etc.

### Case 3

- same than case 2 but open the room setting from a timeline

### Case 4

- same than case 2 and 3 but from the gallery open the image first.
- test the new Forward action like on case 1

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 24
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] You've made a self review of your PR


Test command: ./gradlew :libraries:mediaviewer:impl:testDebugUnitTest --tests "io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerPresenterTest" --tests "io.element.android.libraries.mediaviewer.impl.DefaultMediaViewerEntryPointTest" --tests "io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryPresenterTest" --tests "io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheetTest" --tests "io.element.android.libraries.mediaviewer.impl.DefaultMediaGalleryEntryPointTest" :features:forward:impl:testDebugUnitTest --tests "io.element.android.features.forward.impl.ForwardMessagesViewTest" --tests "io.element.android.features.forward.impl.ForwardMessagesPresenterTest" --tests "io.element.android.features.forward.impl.DefaultForwardEntryPointTest" :features:messages:impl:testDebugUnitTest --tests "io.element.android.features.messages.impl.DefaultMessagesEntryPointTest" :features:roomdetails:impl:testDebugUnitTest --tests "io.element.android.features.roomdetails.impl.DefaultRoomDetailsEntryPointTest" :appnav:testDebugUnitTest --tests "io.element.android.appnav.JoinedRoomLoadedFlowNodeTest"

Fix patch:
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 eb1df5643ef..da09add2dc5 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 -> {
@@ -473,9 +469,8 @@ class LoggedInFlowNode(
                     .callback(object : ShareEntryPoint.Callback {
                         override fun onDone(roomIds: List<RoomId>) {
                             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/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 aab9590a9b2..8b0cca900a1 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,12 +16,14 @@ 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
 import io.element.android.annotations.ContributesNode
 import io.element.android.appnav.di.RoomGraphFactory
 import io.element.android.appnav.room.RoomNavigationTarget
+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
@@ -41,6 +43,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
@@ -53,6 +57,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,
@@ -70,7 +75,6 @@ class JoinedRoomLoadedFlowNode(
     interface Callback : Plugin {
         fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
         fun onPermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
-        fun onForwardedToSingleRoom(roomId: RoomId)
         fun onOpenGlobalNotificationSettings()
     }
 
@@ -128,8 +132,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)
@@ -158,6 +162,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 onDone(roomIds: List<RoomId>) {
+                        backstack.pop()
+                        roomIds.singleOrNull()?.let { roomId ->
+                            callbacks.forEach { it.onOpenRoom(roomId, emptyList()) }
+                        }
+                    }
+                }
+                forwardEntryPoint.nodeBuilder(this, buildContext)
+                    .params(params)
+                    .callback(callback)
+                    .build()
+            }
         }
     }
 
@@ -198,8 +218,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(
@@ -227,6 +251,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/features/forward/api/build.gradle.kts b/features/forward/api/build.gradle.kts
new file mode 100644
index 00000000000..b5c73539d70
--- /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 00000000000..d822c0adb83
--- /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 onDone(roomIds: List<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 00000000000..32da0ed7a29
--- /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 00000000000..55eede7b572
--- /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.SessionScope
+
+@ContributesBinding(SessionScope::class)
+class DefaultForwardEntryPoint : ForwardEntryPoint {
+    override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ForwardEntryPoint.NodeBuilder {
+        val plugins = ArrayList<Plugin>()
+
+        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<ForwardMessagesNode>(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 05a77b61a78..06495cfc24e 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 82%
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 2cf0c6e1e7f..27e95898d2f 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,9 +20,10 @@ 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
+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
@@ -30,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,
@@ -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<Inputs>()
     private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider)
-    private val callbacks = plugins.filterIsInstance<Callback>()
+    private val callbacks = plugins.filterIsInstance<ForwardEntryPoint.Callback>()
 
     override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
         val callback = object : RoomSelectEntryPoint.Callback {
@@ -68,7 +65,7 @@ class ForwardMessagesNode(
             }
 
             override fun onCancel() {
-                navigateUp()
+                onForwardDone(emptyList())
             }
         }
 
@@ -89,16 +86,12 @@ class ForwardMessagesNode(
             val state = presenter.present()
             ForwardMessagesView(
                 state = state,
-                onForwardSuccess = ::onForwardSuccess,
+                onForwardSuccess = ::onForwardDone,
             )
         }
     }
 
-    private fun onForwardSuccess(roomIds: List<RoomId>) {
-        navigateUp()
-        if (roomIds.size == 1) {
-            val targetRoomId = roomIds.first()
-            callbacks.forEach { it.onForwardedToSingleRoom(targetRoomId) }
-        }
+    private fun onForwardDone(roomIds: List<RoomId>) {
+        callbacks.forEach { it.onDone(roomIds) }
     }
 }
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 83%
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 3e3860db3e0..63fbb0bc7cc 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
@@ -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
 
@@ -36,14 +34,14 @@ class ForwardMessagesPresenter(
     private val eventId: EventId = EventId(eventId)
 
     @AssistedFactory
-    interface Factory {
+    fun interface Factory {
         fun create(eventId: String, timelineProvider: TimelineProvider): ForwardMessagesPresenter
     }
 
     private val forwardingActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
 
     fun onRoomSelected(roomIds: List<RoomId>) {
-        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<RoomId>,
-        isForwardMessagesState: MutableState<AsyncAction<List<RoomId>>>,
+        roomIds: List<RoomId>,
     ) = launch {
         suspend {
             timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).getOrThrow()
             roomIds
-        }.runCatchingUpdatingState(isForwardMessagesState)
+        }.runCatchingUpdatingState(forwardingActionState)
     }
 }
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 a1de911c728..a6d93f39da9 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 b1728f46572..f5789549c57 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 9ea76d754f6..214c7958519 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/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 706e292dbbb..8478a57940a 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/build.gradle.kts b/features/messages/impl/build.gradle.kts
index 43efad9da07..ee2d7f48b06 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 82aafcf545a..8807dd77cb7 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
@@ -25,6 +26,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 +35,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 +104,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,
@@ -149,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
@@ -305,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)
@@ -333,13 +343,19 @@ class MessagesFlowNode(
                 } else {
                     timelineController
                 }
-                val inputs = ForwardMessagesNode.Inputs(navTarget.eventId, timelineProvider)
-                val callback = object : ForwardMessagesNode.Callback {
-                    override fun onForwardedToSingleRoom(roomId: RoomId) {
-                        callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
+                val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
+                val callback = object : ForwardEntryPoint.Callback {
+                    override fun onDone(roomIds: List<RoomId>) {
+                        backstack.pop()
+                        roomIds.singleOrNull()?.let { roomId ->
+                            callbacks.forEach { it.openRoom(roomId) }
+                        }
                     }
                 }
-                createNode<ForwardMessagesNode>(buildContext, listOf(inputs, callback))
+                forwardEntryPoint.nodeBuilder(this, buildContext)
+                    .params(params)
+                    .callback(callback)
+                    .build()
             }
             is NavTarget.ReportMessage -> {
                 val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
@@ -485,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/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 0d725938f61..48fcac8ee61 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
@@ -39,7 +40,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
         fun onOpenGlobalNotificationSettings()
         fun onOpenRoom(roomId: RoomId, serverNames: List<String>)
         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 2b4bf10d672..a72f0394b47 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<RoomDetailsEntryPoint.Callback>().forEach { it.forwardEvent(eventId) }
+                    }
                 }
                 mediaGalleryEntryPoint.nodeBuilder(this, buildContext)
                     .callback(callback)
@@ -343,8 +351,12 @@ class RoomDetailsFlowNode(
                         plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onPermalinkClick(data, pushToBackstack) }
                     }
 
-                    override fun onForwardedToSingleRoom(roomId: RoomId) {
-                        plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onForwardedToSingleRoom(roomId) }
+                    override fun forwardEvent(eventId: EventId) {
+                        plugins<RoomDetailsEntryPoint.Callback>().forEach { it.forwardEvent(eventId) }
+                    }
+
+                    override fun openRoom(roomId: RoomId) {
+                        plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onOpenRoom(roomId, emptyList()) }
                     }
                 }
                 return messagesEntryPoint.nodeBuilder(this, buildContext)
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 e2684199205..12d2f5bff53 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<RoomId>) {
+    private fun onShareDone(roomIds: List<RoomId>) {
         callbacks.forEach { it.onDone(roomIds) }
     }
 }
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 5828d60c256..da3adef973b 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 acc18ee1a0b..592b5ab3bad 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<Timeline?>
 }
 
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 841e82f1959..119c1002c86 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 906283c4570..b16de69b98b 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/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 22c29c590bc..35d5a3e5182 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 df7d82c7b29..1a5bf70170a 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,8 +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 SaveOnDisk(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 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 0195e7f39cb..8ce4860c530 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 06a3c6a58f4..6ee31c65200 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<Callback>().forEach {
+            it.onForward(eventId)
+        }
+    }
+
     private fun onItemClick(item: MediaItem.Event) {
         plugins<Callback>().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 ba3d4d5e1f8..3f8a31cc261 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 a48ff93d714..50a7fe9aad6 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 e617829ad0d..76f025006fe 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<Plugin>,
-    private val mediaViewerEntryPoint: MediaViewerEntryPoint
+    private val mediaViewerEntryPoint: MediaViewerEntryPoint,
 ) : BaseFlowNode<MediaGalleryFlowNode.NavTarget>(
     backstack = BackStack(
         initialElement = NavTarget.Root,
@@ -82,6 +82,12 @@ class MediaGalleryFlowNode(
         }
     }
 
+    private fun forwardEvent(eventId: EventId) {
+        plugins<MediaGalleryEntryPoint.Callback>().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 708c423d363..519100b6104 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 c75db00afe7..77e253dfa5b 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 6599411411b..ee874156cdd 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<MediaViewerEntryPoint.Callback>().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 c7b93a227f0..726e9989ce9 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 110054eb20b..791bd1bc8e9 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))


Test patch:
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 f8a18fa24a0..8e73165cb6c 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<Plugin>,
         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/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 00000000000..dabb8af4a13
--- /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 onDone(roomIds: List<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 757e682592e..6850a5ef07f 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 0912fcc75c1..9cecda6973b 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/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 2de761ad694..dc7e5ad5d49 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(),
@@ -115,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/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 f2412e616bc..a9d528ace56 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<String>) = 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/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 00000000000..519789c0030
--- /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<Timeline?> {
+        return timelineFlow.asStateFlow()
+    }
+}
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 0f8b0fedfb4..38040426e16 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 ddc04fc6cea..a1f60cc124c 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 bd4373a4203..3177ed0774d 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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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 2566ef91a45..8727335e6d4 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/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt
index f126dab2b5d..b09aae5d26f 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<EventId, Unit> { }
         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<EventId, Unit> { }
+        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<Timeline.PaginationDirection, Unit> { }
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 6c0148b1245..791527445b7 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()
     }
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 b9964d8d3d5..359eda7a5eb 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<EventId, Unit> { }
         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<EventId, Unit> { }
+        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 <T> ReceiveTurbine<T>.awaitFirstItem(): T {
         return awaitItem()
     }
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 2ecb6f0e65f..9ebcafa2e01 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 ebed9fbaea4..03f88988e53 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 a2ec7507eda..2f643f7ffd6 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 bf10c81b0b6..9248804599f 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 b094052a007..df4212be1ec 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