Add Forward action to MediaDetailsBottomSheet. Closes #5454

Improve API of Callback when forwarding Event.
This commit is contained in:
Benoit Marty 2025-10-28 16:02:37 +01:00 committed by Benoit Marty
parent e9cfce915a
commit 21bae4aee2
35 changed files with 190 additions and 36 deletions

View file

@ -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?>
}

View file

@ -24,5 +24,6 @@ interface MediaGalleryEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onBackClick()
fun onViewInTimeline(eventId: EventId)
fun forwardEvent(eventId: EventId)
}
}

View file

@ -31,6 +31,7 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onDone()
fun onViewInTimeline(eventId: EventId)
fun onForwardEvent(eventId: EventId)
}
data class Params(

View file

@ -33,6 +33,7 @@ dependencies {
implementation(libs.telephoto.flick)
implementation(projects.features.enterprise.api)
implementation(projects.features.forward.api)
implementation(projects.features.viewfolder.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)

View file

@ -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 = {},

View file

@ -17,6 +17,7 @@ sealed interface MediaGalleryEvents {
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
data class Share(val eventId: EventId?) : MediaGalleryEvents
data class Forward(val eventId: EventId) : MediaGalleryEvents
data class SaveOnDisk(val eventId: EventId?) : MediaGalleryEvents
data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents

View file

@ -11,4 +11,5 @@ import io.element.android.libraries.matrix.api.core.EventId
interface MediaGalleryNavigator {
fun onViewInTimelineClick(eventId: EventId)
fun onForwardClick(eventId: EventId)
}

View file

@ -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)

View file

@ -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)

View file

@ -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))
},

View file

@ -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(

View file

@ -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,

View file

@ -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()
}

View file

@ -71,6 +71,12 @@ class MediaViewerNode(
}
}
override fun onForwardClick(eventId: EventId) {
plugins<MediaViewerEntryPoint.Callback>().forEach {
it.onForwardEvent(eventId)
}
}
override fun onItemDeleted() {
onDone()
}

View file

@ -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,

View file

@ -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))

View file

@ -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)

View file

@ -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(

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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()
}