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

@ -251,8 +251,6 @@ class MessagesFlowNode @AssistedInject constructor(
mediaSource = navTarget.mediaSource, mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource, thumbnailSource = navTarget.thumbnailSource,
canShowInfo = true, canShowInfo = true,
canDownload = true,
canShare = true,
) )
val callback = object : MediaViewerEntryPoint.Callback { val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() { override fun onDone() {

View file

@ -36,7 +36,5 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val canShowInfo: Boolean, val canShowInfo: Boolean,
val canDownload: Boolean,
val canShare: Boolean,
) : NodeInputs ) : NodeInputs
} }

View file

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

View file

@ -48,6 +48,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun MediaDetailsBottomSheet( fun MediaDetailsBottomSheet(
state: MediaBottomSheetState.MediaDetailsBottomSheetState, state: MediaBottomSheetState.MediaDetailsBottomSheetState,
onViewInTimeline: (EventId) -> Unit, onViewInTimeline: (EventId) -> Unit,
onShare: (EventId) -> Unit,
onDownload: (EventId) -> Unit,
onDelete: (EventId) -> Unit, onDelete: (EventId) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -92,6 +94,22 @@ fun MediaDetailsBottomSheet(
onViewInTimeline(state.eventId) 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) { if (state.canDelete) {
HorizontalDivider() HorizontalDivider()
ListItem( ListItem(
@ -196,6 +214,8 @@ internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
MediaDetailsBottomSheet( MediaDetailsBottomSheet(
state = aMediaDetailsBottomSheetState(), state = aMediaDetailsBottomSheetState(),
onViewInTimeline = {}, onViewInTimeline = {},
onShare = {},
onDownload = {},
onDelete = {}, onDelete = {},
onDismiss = {}, onDismiss = {},
) )

View file

@ -15,8 +15,8 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo
sealed interface MediaGalleryEvents { sealed interface MediaGalleryEvents {
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
data class Share(val mediaItem: MediaItem.Event) : MediaGalleryEvents data class Share(val eventId: EventId?) : MediaGalleryEvents
data class SaveOnDisk(val mediaItem: MediaItem.Event) : MediaGalleryEvents data class SaveOnDisk(val eventId: EventId?) : MediaGalleryEvents
data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents

View file

@ -117,8 +117,16 @@ class MediaGalleryPresenter @AssistedInject constructor(
timeline.dataOrNull()?.paginate(event.direction) timeline.dataOrNull()?.paginate(event.direction)
} }
is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId) is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId)
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.saveOnDisk(event.mediaItem) is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch {
is MediaGalleryEvents.Share -> coroutineScope.share(event.mediaItem) mediaItems.dataOrNull().find(event.eventId)?.let {
saveOnDisk(it)
}
}
is MediaGalleryEvents.Share -> coroutineScope.launch {
mediaItems.dataOrNull().find(event.eventId)?.let {
share(it)
}
}
is MediaGalleryEvents.ViewInTimeline -> { is MediaGalleryEvents.ViewInTimeline -> {
mediaBottomSheetState = MediaBottomSheetState.Hidden mediaBottomSheetState = MediaBottomSheetState.Hidden
navigator.onViewInTimelineClick(event.eventId) 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) downloadMedia(mediaItem)
.mapCatching { localMedia -> .mapCatching { localMedia ->
localMediaActions.saveOnDisk(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) downloadMedia(mediaItem)
.mapCatching { localMedia -> .mapCatching { localMedia ->
localMediaActions.share(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 -> onViewInTimeline = { eventId ->
state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId)) state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId))
}, },
onShare = { eventId ->
state.eventSink(MediaGalleryEvents.Share(eventId))
},
onDownload = { eventId ->
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
},
onDelete = { eventId -> onDelete = { eventId ->
state.eventSink( state.eventSink(
MediaGalleryEvents.ConfirmDelete( MediaGalleryEvents.ConfirmDelete(
@ -276,11 +282,17 @@ private fun MediaGalleryFilesList(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
file = item, file = item,
onClick = { onItemClick(item) }, onClick = { onItemClick(item) },
onLongClick = {
eventSink(MediaGalleryEvents.OpenInfo(item))
},
) )
is MediaItem.Audio -> AudioItemView( is MediaItem.Audio -> AudioItemView(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
audio = item, audio = item,
onClick = { onItemClick(item) }, onClick = { onItemClick(item) },
onLongClick = {
eventSink(MediaGalleryEvents.OpenInfo(item))
},
) )
is MediaItem.Voice -> { is MediaItem.Voice -> {
val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item) val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item)
@ -288,9 +300,9 @@ private fun MediaGalleryFilesList(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
state = presenter.present(), state = presenter.present(),
voice = item, voice = item,
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) }, onLongClick = {
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) }, eventSink(MediaGalleryEvents.OpenInfo(item))
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) }, },
) )
} }
is MediaItem.DateSeparator -> DateItemView( is MediaItem.DateSeparator -> DateItemView(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -66,8 +66,6 @@ class MediaViewerPresenterTest {
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.canDownload).isTrue()
assertThat(initialState.canShare).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
} }
} }
@ -86,48 +84,6 @@ class MediaViewerPresenterTest {
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isFalse() assertThat(initialState.canShowInfo).isFalse()
assertThat(initialState.canDownload).isTrue()
assertThat(initialState.canShare).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
}
}
@Test
fun `present - initial state cannot share`() = runTest {
val presenter = createMediaViewerPresenter(
canShare = false,
room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
)
)
presenter.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.canDownload).isTrue()
assertThat(initialState.canShare).isFalse()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
}
}
@Test
fun `present - initial state cannot download`() = runTest {
val presenter = createMediaViewerPresenter(
canDownload = false,
room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
)
)
presenter.test {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.canDownload).isFalse()
assertThat(initialState.canShare).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
} }
} }
@ -146,8 +102,6 @@ class MediaViewerPresenterTest {
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.canDownload).isTrue()
assertThat(initialState.canShare).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
} }
} }
@ -167,8 +121,6 @@ class MediaViewerPresenterTest {
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.canShowInfo).isTrue()
assertThat(initialState.canDownload).isTrue()
assertThat(initialState.canShare).isTrue()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
} }
} }
@ -350,8 +302,6 @@ class MediaViewerPresenterTest {
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(), localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
canShowInfo: Boolean = true, canShowInfo: Boolean = true,
canShare: Boolean = true,
canDownload: Boolean = true,
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(), mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
room: MatrixRoom = FakeMatrixRoom( room: MatrixRoom = FakeMatrixRoom(
liveTimeline = FakeTimeline(), liveTimeline = FakeTimeline(),
@ -364,8 +314,6 @@ class MediaViewerPresenterTest {
mediaSource = aMediaSource(), mediaSource = aMediaSource(),
thumbnailSource = null, thumbnailSource = null,
canShowInfo = canShowInfo, canShowInfo = canShowInfo,
canShare = canShare,
canDownload = canDownload,
), ),
localMediaFactory = localMediaFactory, localMediaFactory = localMediaFactory,
mediaLoader = matrixMediaLoader, mediaLoader = matrixMediaLoader,

View file

@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@ -54,16 +55,6 @@ class MediaViewerViewTest {
testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith) testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith)
} }
@Test
fun `clicking on save emit expected Event`() {
testMenuAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
}
@Test
fun `clicking on share emit expected Event`() {
testMenuAction(CommonStrings.action_share, MediaViewerEvents.Share)
}
private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) { private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
val eventsRecorder = EventsRecorder<MediaViewerEvents>() val eventsRecorder = EventsRecorder<MediaViewerEvents>()
rule.setMediaViewerView( rule.setMediaViewerView(
@ -80,6 +71,32 @@ class MediaViewerViewTest {
eventsRecorder.assertSingle(expectedEvent) eventsRecorder.assertSingle(expectedEvent)
} }
@Test
fun `clicking on save emit expected Event`() {
testBottomSheetAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
}
@Test
fun `clicking on share emit expected Event`() {
testBottomSheetAction(CommonStrings.action_share, MediaViewerEvents.Share)
}
private fun testBottomSheetAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
rule.setMediaViewerView(
aMediaViewerState(
downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, anImageMediaInfo())
),
mediaInfo = anImageMediaInfo(),
mediaBottomSheetState = aMediaDetailsBottomSheetState(),
eventSink = eventsRecorder
),
)
rule.clickOn(contentDescriptionRes)
eventsRecorder.assertSingle(expectedEvent)
}
@Test @Test
fun `clicking on image hides the overlay`() { fun `clicking on image hides the overlay`() {
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)