Merge pull request #4071 from element-hq/feature/bma/galleryUiTweak
Media gallery UI update
This commit is contained in:
@@ -251,8 +251,6 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
mediaSource = navTarget.mediaSource,
|
||||
thumbnailSource = navTarget.thumbnailSource,
|
||||
canShowInfo = true,
|
||||
canDownload = true,
|
||||
canShare = true,
|
||||
)
|
||||
val callback = object : MediaViewerEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
|
||||
@@ -158,7 +158,7 @@ enum class FeatureFlags(
|
||||
key = "feature.media_gallery",
|
||||
title = "Allow user to open the media gallery",
|
||||
description = null,
|
||||
defaultValue = { buildMeta -> buildMeta.buildType != BuildType.RELEASE },
|
||||
defaultValue = { true },
|
||||
isFinished = false,
|
||||
),
|
||||
EventCache(
|
||||
|
||||
@@ -36,7 +36,5 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val canShowInfo: Boolean,
|
||||
val canDownload: Boolean,
|
||||
val canShare: Boolean,
|
||||
) : NodeInputs
|
||||
}
|
||||
|
||||
@@ -59,8 +59,6 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
|
||||
mediaSource = MediaSource(url = avatarUrl),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = false,
|
||||
canDownload = false,
|
||||
canShare = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
|
||||
@@ -12,10 +12,11 @@ import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
|
||||
fun aMediaDetailsBottomSheetState(
|
||||
dateSentFull: String = "December 6, 2024 at 12:59",
|
||||
canDelete: Boolean = true,
|
||||
): MediaBottomSheetState.MediaDetailsBottomSheetState {
|
||||
return MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
eventId = EventId("\$eventId"),
|
||||
canDelete = true,
|
||||
canDelete = canDelete,
|
||||
mediaInfo = anImageMediaInfo(
|
||||
senderName = "Alice",
|
||||
dateSentFull = dateSentFull,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.PageTitle
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
@@ -106,7 +107,8 @@ fun MediaGalleryView(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
.fillMaxSize()
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier
|
||||
@@ -151,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(
|
||||
@@ -274,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)
|
||||
@@ -286,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(
|
||||
@@ -426,6 +440,7 @@ private fun EmptyContent(
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
OnboardingBackground()
|
||||
PageTitle(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -122,8 +122,6 @@ class MediaGalleryRootNode @AssistedInject constructor(
|
||||
mediaSource = navTarget.mediaSource,
|
||||
thumbnailSource = navTarget.thumbnailSource,
|
||||
canShowInfo = true,
|
||||
canDownload = true,
|
||||
canShare = true,
|
||||
)
|
||||
)
|
||||
.callback(callback)
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
@@ -84,6 +85,14 @@ private fun VideoInfoRow(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
ElementTheme.colors.bgCanvasDefault.copy(alpha = 0f),
|
||||
ElementTheme.colors.bgCanvasDefault,
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.details
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MediaDeleteConfirmationBottomSheetTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on Cancel invokes expected callback`() {
|
||||
val state = aMediaDeleteConfirmationState()
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMediaDeleteConfirmationBottomSheet(
|
||||
state = state,
|
||||
onDismiss = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on Remove invokes expected callback`() {
|
||||
val state = aMediaDeleteConfirmationState()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDeleteConfirmationBottomSheet(
|
||||
state = state,
|
||||
onDelete = callback,
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
|
||||
rule.clickOn(CommonStrings.action_remove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMediaDeleteConfirmationBottomSheet(
|
||||
state: MediaBottomSheetState.MediaDeleteConfirmationState,
|
||||
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
MediaDeleteConfirmationBottomSheet(
|
||||
state = state,
|
||||
onDelete = onDelete,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.details
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MediaDetailsBottomSheetTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on View in timeline invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onViewInTimeline = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_view_in_timeline)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on Share invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onShare = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_share)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on Save invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onDownload = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on Remove invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onDelete = callback,
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
|
||||
rule.clickOn(CommonStrings.action_remove)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `Remove is not present if canDelete is false`() {
|
||||
val state = aMediaDetailsBottomSheetState(
|
||||
canDelete = false,
|
||||
)
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMediaDetailsBottomSheet(
|
||||
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
|
||||
onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
MediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onViewInTimeline = onViewInTimeline,
|
||||
onShare = onShare,
|
||||
onDownload = onDownload,
|
||||
onDelete = onDelete,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,6 @@ class MediaViewerPresenterTest {
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isTrue()
|
||||
assertThat(initialState.canDownload).isTrue()
|
||||
assertThat(initialState.canShare).isTrue()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
}
|
||||
@@ -86,48 +84,6 @@ class MediaViewerPresenterTest {
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -146,8 +102,6 @@ class MediaViewerPresenterTest {
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isTrue()
|
||||
assertThat(initialState.canDownload).isTrue()
|
||||
assertThat(initialState.canShare).isTrue()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
}
|
||||
@@ -167,8 +121,6 @@ class MediaViewerPresenterTest {
|
||||
assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
assertThat(initialState.canShowInfo).isTrue()
|
||||
assertThat(initialState.canDownload).isTrue()
|
||||
assertThat(initialState.canShare).isTrue()
|
||||
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
}
|
||||
@@ -350,8 +302,6 @@ class MediaViewerPresenterTest {
|
||||
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
canShowInfo: Boolean = true,
|
||||
canShare: Boolean = true,
|
||||
canDownload: Boolean = true,
|
||||
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
|
||||
room: MatrixRoom = FakeMatrixRoom(
|
||||
liveTimeline = FakeTimeline(),
|
||||
@@ -364,8 +314,6 @@ class MediaViewerPresenterTest {
|
||||
mediaSource = aMediaSource(),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = canShowInfo,
|
||||
canShare = canShare,
|
||||
canDownload = canDownload,
|
||||
),
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaLoader = matrixMediaLoader,
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
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.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
@@ -54,16 +55,6 @@ class MediaViewerViewTest {
|
||||
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) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
rule.setMediaViewerView(
|
||||
@@ -80,6 +71,32 @@ class MediaViewerViewTest {
|
||||
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
|
||||
fun `clicking on image hides the overlay`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>(expectEvents = false)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user