Move share and download actions to the bottom sheet

This commit is contained in:
Benoit Marty 2024-12-19 16:20:41 +01:00
parent 194b1a8c56
commit f8b2f24962
17 changed files with 127 additions and 178 deletions

View file

@ -59,8 +59,6 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null,
canShowInfo = false,
canDownload = false,
canShare = false,
)
)
}

View file

@ -48,6 +48,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun MediaDetailsBottomSheet(
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
onViewInTimeline: (EventId) -> Unit,
onShare: (EventId) -> Unit,
onDownload: (EventId) -> Unit,
onDelete: (EventId) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
@ -92,6 +94,22 @@ fun MediaDetailsBottomSheet(
onViewInTimeline(state.eventId)
}
)
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ShareAndroid())),
headlineContent = { Text(stringResource(CommonStrings.action_share)) },
style = ListItemStyle.Primary,
onClick = {
onShare(state.eventId)
}
)
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())),
headlineContent = { Text(stringResource(CommonStrings.action_save)) },
style = ListItemStyle.Primary,
onClick = {
onDownload(state.eventId)
}
)
if (state.canDelete) {
HorizontalDivider()
ListItem(
@ -196,6 +214,8 @@ internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
MediaDetailsBottomSheet(
state = aMediaDetailsBottomSheetState(),
onViewInTimeline = {},
onShare = {},
onDownload = {},
onDelete = {},
onDismiss = {},
)

View file

@ -15,8 +15,8 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo
sealed interface MediaGalleryEvents {
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
data class Share(val mediaItem: MediaItem.Event) : MediaGalleryEvents
data class SaveOnDisk(val mediaItem: MediaItem.Event) : MediaGalleryEvents
data class Share(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

@ -117,8 +117,16 @@ class MediaGalleryPresenter @AssistedInject constructor(
timeline.dataOrNull()?.paginate(event.direction)
}
is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId)
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.saveOnDisk(event.mediaItem)
is MediaGalleryEvents.Share -> coroutineScope.share(event.mediaItem)
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch {
mediaItems.dataOrNull().find(event.eventId)?.let {
saveOnDisk(it)
}
}
is MediaGalleryEvents.Share -> coroutineScope.launch {
mediaItems.dataOrNull().find(event.eventId)?.let {
share(it)
}
}
is MediaGalleryEvents.ViewInTimeline -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
navigator.onViewInTimelineClick(event.eventId)
@ -221,7 +229,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
}
}
private fun CoroutineScope.saveOnDisk(mediaItem: MediaItem.Event) = launch {
private suspend fun saveOnDisk(mediaItem: MediaItem.Event) {
downloadMedia(mediaItem)
.mapCatching { localMedia ->
localMediaActions.saveOnDisk(localMedia)
@ -236,7 +244,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
}
}
private fun CoroutineScope.share(mediaItem: MediaItem.Event) = launch {
private suspend fun share(mediaItem: MediaItem.Event) {
downloadMedia(mediaItem)
.mapCatching { localMedia ->
localMediaActions.share(localMedia)
@ -255,3 +263,11 @@ class MediaGalleryPresenter @AssistedInject constructor(
}
}
}
private fun List<MediaItem>?.find(eventId: EventId?): MediaItem.Event? {
if (this == null || eventId == null) {
return null
}
return filterIsInstance<MediaItem.Event>()
.firstOrNull { it.eventId() == eventId }
}

View file

@ -153,6 +153,12 @@ fun MediaGalleryView(
onViewInTimeline = { eventId ->
state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId))
},
onShare = { eventId ->
state.eventSink(MediaGalleryEvents.Share(eventId))
},
onDownload = { eventId ->
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
},
onDelete = { eventId ->
state.eventSink(
MediaGalleryEvents.ConfirmDelete(
@ -276,11 +282,17 @@ private fun MediaGalleryFilesList(
modifier = Modifier.animateItem(),
file = item,
onClick = { onItemClick(item) },
onLongClick = {
eventSink(MediaGalleryEvents.OpenInfo(item))
},
)
is MediaItem.Audio -> AudioItemView(
modifier = Modifier.animateItem(),
audio = item,
onClick = { onItemClick(item) },
onLongClick = {
eventSink(MediaGalleryEvents.OpenInfo(item))
},
)
is MediaItem.Voice -> {
val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item)
@ -288,9 +300,9 @@ private fun MediaGalleryFilesList(
modifier = Modifier.animateItem(),
state = presenter.present(),
voice = item,
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) },
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
onLongClick = {
eventSink(MediaGalleryEvents.OpenInfo(item))
},
)
}
is MediaItem.DateSeparator -> DateItemView(

View file

@ -122,8 +122,6 @@ class MediaGalleryRootNode @AssistedInject constructor(
mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource,
canShowInfo = true,
canDownload = true,
canShare = true,
)
)
.callback(callback)

View file

@ -7,8 +7,9 @@
package io.element.android.libraries.mediaviewer.impl.gallery.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -41,6 +42,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
fun AudioItemView(
audio: MediaItem.Audio,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -52,6 +54,7 @@ fun AudioItemView(
FilenameRow(
audio = audio,
onClick = onClick,
onLongClick = onLongClick,
)
val caption = audio.mediaInfo.caption
if (caption != null) {
@ -63,10 +66,12 @@ fun AudioItemView(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FilenameRow(
audio: MediaItem.Audio,
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
Row(
modifier = Modifier
@ -75,7 +80,7 @@ private fun FilenameRow(
color = ElementTheme.colors.bgSubtleSecondary,
shape = RoundedCornerShape(12.dp),
)
.clickable { onClick() }
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.fillMaxWidth()
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
@ -119,5 +124,6 @@ internal fun AudioItemViewPreview(
AudioItemView(
audio = audio,
onClick = {},
onLongClick = {},
)
}

View file

@ -7,8 +7,9 @@
package io.element.android.libraries.mediaviewer.impl.gallery.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -40,6 +41,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
fun FileItemView(
file: MediaItem.File,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -51,6 +53,7 @@ fun FileItemView(
FilenameRow(
file = file,
onClick = onClick,
onLongClick = onLongClick,
)
val caption = file.mediaInfo.caption
if (caption != null) {
@ -62,10 +65,12 @@ fun FileItemView(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FilenameRow(
file: MediaItem.File,
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
Row(
modifier = Modifier
@ -74,7 +79,7 @@ private fun FilenameRow(
color = ElementTheme.colors.bgSubtleSecondary,
shape = RoundedCornerShape(12.dp),
)
.clickable { onClick() }
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.fillMaxWidth()
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
@ -118,5 +123,6 @@ internal fun FileItemViewPreview(
FileItemView(
file = file,
onClick = {},
onLongClick = {},
)
}

View file

@ -7,9 +7,10 @@
package io.element.android.libraries.mediaviewer.impl.gallery.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -58,9 +59,7 @@ import kotlinx.coroutines.delay
fun VoiceItemView(
state: VoiceMessageState,
voice: MediaItem.Voice,
onShareClick: () -> Unit,
onDownloadClick: () -> Unit,
onInfoClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -72,6 +71,7 @@ fun VoiceItemView(
VoiceInfoRow(
state = state,
voice = voice,
onLongClick = onLongClick,
)
val caption = voice.mediaInfo.caption
if (caption != null) {
@ -79,19 +79,16 @@ fun VoiceItemView(
} else {
Spacer(modifier = Modifier.height(16.dp))
}
ActionIconsRow(
onShareClick = onShareClick,
onDownloadClick = onDownloadClick,
onInfoClick = onInfoClick,
)
HorizontalDivider()
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun VoiceInfoRow(
state: VoiceMessageState,
voice: MediaItem.Voice,
onLongClick: () -> Unit,
) {
fun playPause() {
state.eventSink(VoiceMessageEvents.PlayPause)
@ -104,6 +101,7 @@ private fun VoiceInfoRow(
color = ElementTheme.colors.bgSubtleSecondary,
shape = RoundedCornerShape(12.dp),
)
.combinedClickable(onClick = {}, onLongClick = onLongClick)
.fillMaxWidth()
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
@ -257,43 +255,6 @@ private fun CustomIconButton(
)
}
@Composable
private fun ActionIconsRow(
onShareClick: () -> Unit,
onDownloadClick: () -> Unit,
onInfoClick: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
IconButton(
onClick = onShareClick,
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = null,
)
}
IconButton(
onClick = onDownloadClick,
) {
Icon(
imageVector = CompoundIcons.Download(),
contentDescription = null,
)
}
IconButton(
onClick = onInfoClick,
) {
Icon(
imageVector = CompoundIcons.Info(),
contentDescription = null,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun VoiceItemViewPreview(
@ -302,9 +263,7 @@ internal fun VoiceItemViewPreview(
VoiceItemView(
state = aVoiceMessageState(),
voice = voice,
onShareClick = {},
onDownloadClick = {},
onInfoClick = {},
onLongClick = {},
)
}
@ -316,8 +275,6 @@ internal fun VoiceItemViewPlayPreview(
VoiceItemView(
state = state,
voice = aMediaItemVoice(),
onShareClick = {},
onDownloadClick = {},
onInfoClick = {},
onLongClick = {},
)
}

View file

@ -83,9 +83,18 @@ class MediaViewerPresenter @AssistedInject constructor(
when (mediaViewerEvents) {
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
MediaViewerEvents.ClearLoadingError -> localMedia.value = AsyncData.Uninitialized
MediaViewerEvents.SaveOnDisk -> coroutineScope.saveOnDisk(localMedia.value)
MediaViewerEvents.Share -> coroutineScope.share(localMedia.value)
MediaViewerEvents.OpenWith -> coroutineScope.open(localMedia.value)
MediaViewerEvents.SaveOnDisk -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.saveOnDisk(localMedia.value)
}
MediaViewerEvents.Share -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.share(localMedia.value)
}
MediaViewerEvents.OpenWith -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.open(localMedia.value)
}
is MediaViewerEvents.Delete -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden
coroutineScope.delete(mediaViewerEvents.eventId)
@ -126,8 +135,6 @@ class MediaViewerPresenter @AssistedInject constructor(
downloadedMedia = localMedia.value,
snackbarMessage = snackbarMessage,
canShowInfo = inputs.canShowInfo,
canDownload = inputs.canDownload,
canShare = inputs.canShare,
mediaBottomSheetState = mediaBottomSheetState,
eventSink = ::handleEvents
)

View file

@ -22,8 +22,6 @@ data class MediaViewerState(
val downloadedMedia: AsyncData<LocalMedia>,
val snackbarMessage: SnackbarMessage?,
val canShowInfo: Boolean,
val canDownload: Boolean,
val canShare: Boolean,
val mediaBottomSheetState: MediaBottomSheetState,
val eventSink: (MediaViewerEvents) -> Unit,
)

View file

@ -91,8 +91,6 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
),
mediaInfo = it,
canShowInfo = false,
canDownload = false,
canShare = false,
)
},
aMediaViewerState(
@ -118,8 +116,6 @@ fun aMediaViewerState(
downloadedMedia: AsyncData<LocalMedia> = AsyncData.Uninitialized,
mediaInfo: MediaInfo = anImageMediaInfo(),
canShowInfo: Boolean = true,
canDownload: Boolean = true,
canShare: Boolean = true,
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
eventSink: (MediaViewerEvents) -> Unit = {},
) = MediaViewerState(
@ -129,8 +125,6 @@ fun aMediaViewerState(
downloadedMedia = downloadedMedia,
snackbarMessage = null,
canShowInfo = canShowInfo,
canDownload = canDownload,
canShare = canShare,
mediaBottomSheetState = mediaBottomSheetState,
eventSink = eventSink,
)

View file

@ -119,8 +119,6 @@ fun MediaViewerView(
) {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success,
canDownload = state.canDownload,
canShare = state.canShare,
mimeType = state.mediaInfo.mimeType,
senderName = state.mediaInfo.senderName,
dateSent = state.mediaInfo.dateSent,
@ -148,6 +146,12 @@ fun MediaViewerView(
onViewInTimeline = {
state.eventSink(MediaViewerEvents.ViewInTimeline(it))
},
onShare = {
state.eventSink(MediaViewerEvents.Share)
},
onDownload = {
state.eventSink(MediaViewerEvents.SaveOnDisk)
},
onDelete = { eventId ->
state.eventSink(MediaViewerEvents.ConfirmDelete(eventId))
},
@ -313,8 +317,6 @@ private fun rememberShowProgress(downloadedMedia: AsyncData<LocalMedia>): Boolea
@Composable
private fun MediaViewerTopBar(
actionsEnabled: Boolean,
canDownload: Boolean,
canShare: Boolean,
mimeType: String,
senderName: String?,
dateSent: String?,
@ -348,19 +350,6 @@ private fun MediaViewerTopBar(
),
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
if (canShare) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
) {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(id = CommonStrings.action_share)
)
}
}
IconButton(
enabled = actionsEnabled,
onClick = {
@ -378,19 +367,6 @@ private fun MediaViewerTopBar(
)
}
}
if (canDownload) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.SaveOnDisk)
},
) {
Icon(
imageVector = CompoundIcons.Download(),
contentDescription = stringResource(id = CommonStrings.action_save),
)
}
}
if (canShowInfo) {
IconButton(
onClick = onInfoClick,