mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 20:01:55 +00:00
Merge pull request #6643 from element-hq/feature/bma/updateMediaViewer
Update media viewer UI
This commit is contained in:
+2
-1
@@ -35,6 +35,7 @@ import io.element.android.features.verifysession.impl.emoji.toEmojiResource
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||
import io.element.android.libraries.ui.strings.Strings
|
||||
|
||||
@Composable
|
||||
internal fun VerificationContentVerifying(
|
||||
@@ -49,7 +50,7 @@ internal fun VerificationContentVerifying(
|
||||
) {
|
||||
when (data) {
|
||||
is SessionVerificationData.Decimals -> {
|
||||
val text = data.decimals.joinToString(separator = " - ")
|
||||
val text = data.decimals.joinToString(separator = Strings.NICE_SEPARATOR)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ enum class AvatarSize(val dp: Dp) {
|
||||
KnockRequestItem(52.dp),
|
||||
KnockRequestBanner(32.dp),
|
||||
|
||||
MediaSender(32.dp),
|
||||
MediaSender(52.dp),
|
||||
|
||||
DmCreationConfirmation(64.dp),
|
||||
|
||||
|
||||
+7
-7
@@ -15,16 +15,16 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
sealed interface MediaBottomSheetState {
|
||||
data object Hidden : MediaBottomSheetState
|
||||
|
||||
data class MediaDeleteConfirmationState(
|
||||
val eventId: EventId,
|
||||
val mediaInfo: MediaInfo,
|
||||
val thumbnailSource: MediaSource?,
|
||||
) : MediaBottomSheetState
|
||||
|
||||
data class MediaDetailsBottomSheetState(
|
||||
data class Details(
|
||||
val eventId: EventId?,
|
||||
val canDelete: Boolean,
|
||||
val mediaInfo: MediaInfo,
|
||||
val thumbnailSource: MediaSource?,
|
||||
) : MediaBottomSheetState
|
||||
|
||||
data class DeleteConfirmation(
|
||||
val eventId: EventId,
|
||||
val mediaInfo: MediaInfo,
|
||||
val thumbnailSource: MediaSource?,
|
||||
) : MediaBottomSheetState
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.details
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
|
||||
open class MediaBottomSheetStateDeleteConfirmationProvider : PreviewParameterProvider<MediaBottomSheetState.DeleteConfirmation> {
|
||||
override val values: Sequence<MediaBottomSheetState.DeleteConfirmation>
|
||||
get() = sequenceOf(
|
||||
aMediaBottomSheetStateDeleteConfirmation(),
|
||||
aMediaBottomSheetStateDeleteConfirmation(
|
||||
thumbnailSource = MediaSource("url_thumbnail")
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aMediaBottomSheetStateDeleteConfirmation(
|
||||
mediaInfo: MediaInfo = anImageMediaInfo(
|
||||
senderName = "Alice",
|
||||
),
|
||||
thumbnailSource: MediaSource? = null,
|
||||
) = MediaBottomSheetState.DeleteConfirmation(
|
||||
eventId = EventId("\$eventId"),
|
||||
mediaInfo = mediaInfo,
|
||||
thumbnailSource = thumbnailSource,
|
||||
)
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.details
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
|
||||
open class MediaBottomSheetStateDetailsProvider : PreviewParameterProvider<MediaBottomSheetState.Details> {
|
||||
override val values: Sequence<MediaBottomSheetState.Details>
|
||||
get() = sequenceOf(
|
||||
aMediaBottomSheetStateDetails(),
|
||||
aMediaBottomSheetStateDetails(
|
||||
canDelete = false,
|
||||
),
|
||||
aMediaBottomSheetStateDetails(
|
||||
mediaInfo = anApkMediaInfo(
|
||||
dateSentFull = "December 6, 2024 at 12:59",
|
||||
),
|
||||
),
|
||||
aMediaBottomSheetStateDetails(
|
||||
eventId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aMediaBottomSheetStateDetails(
|
||||
eventId: EventId? = EventId($$"$eventId"),
|
||||
canDelete: Boolean = true,
|
||||
mediaInfo: MediaInfo = anImageMediaInfo(
|
||||
senderName = "Alice",
|
||||
dateSentFull = "December 6, 2024 at 12:59",
|
||||
),
|
||||
) = MediaBottomSheetState.Details(
|
||||
eventId = eventId,
|
||||
canDelete = canDelete,
|
||||
mediaInfo = mediaInfo,
|
||||
thumbnailSource = null,
|
||||
)
|
||||
+9
-5
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
@@ -45,11 +46,12 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.mediaviewer.impl.R
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.strings.Strings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MediaDeleteConfirmationBottomSheet(
|
||||
state: MediaBottomSheetState.MediaDeleteConfirmationState,
|
||||
state: MediaBottomSheetState.DeleteConfirmation,
|
||||
onDelete: (EventId) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -105,7 +107,7 @@ fun MediaDeleteConfirmationBottomSheet(
|
||||
|
||||
@Composable
|
||||
private fun MediaRow(
|
||||
state: MediaBottomSheetState.MediaDeleteConfirmationState,
|
||||
state: MediaBottomSheetState.DeleteConfirmation,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -148,7 +150,7 @@ private fun MediaRow(
|
||||
)
|
||||
// Info
|
||||
Text(
|
||||
text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize,
|
||||
text = state.mediaInfo.mimeType + Strings.NICE_SEPARATOR + state.mediaInfo.formattedFileSize,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -160,9 +162,11 @@ private fun MediaRow(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MediaDeleteConfirmationBottomSheetPreview() = ElementPreview {
|
||||
internal fun MediaDeleteConfirmationBottomSheetPreview(
|
||||
@PreviewParameter(provider = MediaBottomSheetStateDeleteConfirmationProvider::class) state: MediaBottomSheetState.DeleteConfirmation,
|
||||
) = ElementPreview {
|
||||
MediaDeleteConfirmationBottomSheet(
|
||||
state = aMediaDeleteConfirmationState(),
|
||||
state = state,
|
||||
onDelete = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
+108
-58
@@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.details
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -24,10 +25,15 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
@@ -45,15 +51,20 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.impl.R
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.strings.Strings
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=2229-149220
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MediaDetailsBottomSheet(
|
||||
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
|
||||
state: MediaBottomSheetState.Details,
|
||||
onViewInTimeline: (EventId) -> Unit,
|
||||
onShare: (EventId) -> Unit,
|
||||
onForward: (EventId) -> Unit,
|
||||
onDownload: (EventId) -> Unit,
|
||||
onOpenWith: (EventId) -> Unit,
|
||||
onDelete: (EventId) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -66,9 +77,8 @@ fun MediaDetailsBottomSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Title()
|
||||
Section(
|
||||
title = stringResource(R.string.screen_media_details_uploaded_by),
|
||||
) {
|
||||
@@ -86,57 +96,75 @@ fun MediaDetailsBottomSheet(
|
||||
)
|
||||
SectionText(
|
||||
title = stringResource(R.string.screen_media_details_file_format),
|
||||
text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize,
|
||||
text = state.mediaInfo.mimeType + Strings.NICE_SEPARATOR + state.mediaInfo.formattedFileSize,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
if (state.eventId != null) {
|
||||
Column {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VisibilityOn())),
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_view_in_timeline)) },
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = {
|
||||
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.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_download)) },
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = {
|
||||
onDownload(state.eventId)
|
||||
}
|
||||
)
|
||||
val mimeType = state.mediaInfo.mimeType
|
||||
val icon = when (mimeType) {
|
||||
MimeTypes.Apk ->
|
||||
ListItemContent.Icon(IconSource.Resource(R.drawable.ic_apk_install))
|
||||
else ->
|
||||
ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut()))
|
||||
}
|
||||
val wording = when (mimeType) {
|
||||
MimeTypes.Apk -> stringResource(id = CommonStrings.common_install_apk_android)
|
||||
else -> stringResource(id = CommonStrings.action_open_with)
|
||||
}
|
||||
ListItem(
|
||||
leadingContent = icon,
|
||||
headlineContent = { Text(wording) },
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = {
|
||||
onOpenWith(state.eventId)
|
||||
}
|
||||
)
|
||||
if (state.canDelete) {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VisibilityOn())),
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_view_in_timeline)) },
|
||||
style = ListItemStyle.Primary,
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())),
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_delete)) },
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = {
|
||||
onViewInTimeline(state.eventId)
|
||||
onDelete(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.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)) },
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = {
|
||||
onDownload(state.eventId)
|
||||
}
|
||||
)
|
||||
if (state.canDelete) {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())),
|
||||
headlineContent = { Text(stringResource(CommonStrings.action_remove)) },
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = {
|
||||
onDelete(state.eventId)
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,27 +195,46 @@ private fun SenderRow(
|
||||
.weight(1f),
|
||||
) {
|
||||
// Name
|
||||
val bestName = mediaInfo.senderName ?: mediaInfo.senderId?.value.orEmpty()
|
||||
val avatarColors = AvatarColorsProvider.provide(id)
|
||||
Text(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = mediaInfo.senderName.orEmpty(),
|
||||
text = bestName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = avatarColors.foreground,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
// Id
|
||||
Text(
|
||||
text = mediaInfo.senderId?.value.orEmpty(),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
if (!mediaInfo.senderName.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = mediaInfo.senderId?.value.orEmpty(),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Title() {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp)
|
||||
.semantics {
|
||||
heading()
|
||||
},
|
||||
text = stringResource(R.string.screen_media_details_title),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Section(
|
||||
title: String,
|
||||
@@ -196,12 +243,12 @@ private fun Section(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
text = title,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
content()
|
||||
@@ -224,13 +271,16 @@ private fun SectionText(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
|
||||
internal fun MediaDetailsBottomSheetPreview(
|
||||
@PreviewParameter(MediaBottomSheetStateDetailsProvider::class) state: MediaBottomSheetState.Details,
|
||||
) = ElementPreview {
|
||||
MediaDetailsBottomSheet(
|
||||
state = aMediaDetailsBottomSheetState(),
|
||||
state = state,
|
||||
onViewInTimeline = {},
|
||||
onShare = {},
|
||||
onForward = {},
|
||||
onDownload = {},
|
||||
onOpenWith = {},
|
||||
onDelete = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.details
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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 = canDelete,
|
||||
mediaInfo = anImageMediaInfo(
|
||||
senderName = "Alice",
|
||||
dateSentFull = dateSentFull,
|
||||
),
|
||||
thumbnailSource = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun aMediaDeleteConfirmationState(): MediaBottomSheetState.MediaDeleteConfirmationState {
|
||||
return MediaBottomSheetState.MediaDeleteConfirmationState(
|
||||
eventId = EventId("\$eventId"),
|
||||
mediaInfo = anImageMediaInfo(
|
||||
senderName = "Alice",
|
||||
),
|
||||
thumbnailSource = null,
|
||||
)
|
||||
}
|
||||
+12
-11
@@ -14,21 +14,22 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||
|
||||
sealed interface MediaGalleryEvents {
|
||||
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
|
||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
|
||||
data class Share(val eventId: EventId) : MediaGalleryEvents
|
||||
data class 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
|
||||
sealed interface MediaGalleryEvent {
|
||||
data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvent
|
||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvent
|
||||
data class Share(val eventId: EventId) : MediaGalleryEvent
|
||||
data class Forward(val eventId: EventId) : MediaGalleryEvent
|
||||
data class SaveOnDisk(val eventId: EventId) : MediaGalleryEvent
|
||||
data class OpenWith(val eventId: EventId) : MediaGalleryEvent
|
||||
data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvent
|
||||
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvent
|
||||
|
||||
data class ConfirmDelete(
|
||||
val eventId: EventId,
|
||||
val mediaInfo: MediaInfo,
|
||||
val thumbnailSource: MediaSource?,
|
||||
) : MediaGalleryEvents
|
||||
) : MediaGalleryEvent
|
||||
|
||||
data object CloseBottomSheet : MediaGalleryEvents
|
||||
data class Delete(val eventId: EventId) : MediaGalleryEvents
|
||||
data object CloseBottomSheet : MediaGalleryEvent
|
||||
data class Delete(val eventId: EventId) : MediaGalleryEvent
|
||||
}
|
||||
+30
-13
@@ -88,39 +88,45 @@ class MediaGalleryPresenter(
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
localMediaActions.Configure()
|
||||
|
||||
fun handleEvent(event: MediaGalleryEvents) {
|
||||
fun handleEvent(event: MediaGalleryEvent) {
|
||||
when (event) {
|
||||
is MediaGalleryEvents.ChangeMode -> {
|
||||
is MediaGalleryEvent.ChangeMode -> {
|
||||
mode = event.mode
|
||||
}
|
||||
is MediaGalleryEvents.LoadMore -> coroutineScope.launch {
|
||||
is MediaGalleryEvent.LoadMore -> coroutineScope.launch {
|
||||
mediaGalleryDataSource.loadMore(event.direction)
|
||||
}
|
||||
is MediaGalleryEvents.Delete -> coroutineScope.launch {
|
||||
is MediaGalleryEvent.Delete -> coroutineScope.launch {
|
||||
mediaGalleryDataSource.deleteItem(event.eventId)
|
||||
}
|
||||
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch {
|
||||
is MediaGalleryEvent.SaveOnDisk -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
||||
saveOnDisk(it)
|
||||
}
|
||||
}
|
||||
is MediaGalleryEvents.Share -> coroutineScope.launch {
|
||||
is MediaGalleryEvent.OpenWith -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
||||
openWith(it)
|
||||
}
|
||||
}
|
||||
is MediaGalleryEvent.Share -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
||||
share(it)
|
||||
}
|
||||
}
|
||||
is MediaGalleryEvents.Forward -> {
|
||||
is MediaGalleryEvent.Forward -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
navigator.onForwardClick(event.eventId)
|
||||
}
|
||||
is MediaGalleryEvents.ViewInTimeline -> {
|
||||
is MediaGalleryEvent.ViewInTimeline -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
navigator.onViewInTimelineClick(event.eventId)
|
||||
}
|
||||
is MediaGalleryEvents.OpenInfo -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
is MediaGalleryEvent.OpenInfo -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Details(
|
||||
eventId = event.mediaItem.eventId(),
|
||||
canDelete = when (event.mediaItem.mediaInfo().senderId) {
|
||||
null -> false
|
||||
@@ -137,14 +143,14 @@ class MediaGalleryPresenter(
|
||||
},
|
||||
)
|
||||
}
|
||||
is MediaGalleryEvents.ConfirmDelete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState(
|
||||
is MediaGalleryEvent.ConfirmDelete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.DeleteConfirmation(
|
||||
eventId = event.eventId,
|
||||
mediaInfo = event.mediaInfo,
|
||||
thumbnailSource = event.thumbnailSource,
|
||||
)
|
||||
}
|
||||
MediaGalleryEvents.CloseBottomSheet -> {
|
||||
MediaGalleryEvent.CloseBottomSheet -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
}
|
||||
}
|
||||
@@ -200,6 +206,17 @@ class MediaGalleryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openWith(mediaItem: MediaItem.Event) {
|
||||
downloadMedia(mediaItem)
|
||||
.mapCatchingExceptions { localMedia ->
|
||||
localMediaActions.open(localMedia)
|
||||
}
|
||||
.onFailure {
|
||||
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
||||
snackbarDispatcher.post(snackbarMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mediaActionsError(throwable: Throwable): Int {
|
||||
return if (throwable is ActivityNotFoundException) {
|
||||
R.string.error_no_compatible_app_found
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ data class MediaGalleryState(
|
||||
val groupedMediaItems: AsyncData<GroupedMediaItems>,
|
||||
val mediaBottomSheetState: MediaBottomSheetState,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (MediaGalleryEvents) -> Unit,
|
||||
val eventSink: (MediaGalleryEvent) -> Unit,
|
||||
)
|
||||
|
||||
enum class MediaGalleryMode(val stringResource: Int) {
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.media.WaveFormSamples
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails
|
||||
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
|
||||
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio
|
||||
@@ -79,7 +79,7 @@ open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryStat
|
||||
)
|
||||
),
|
||||
),
|
||||
aMediaGalleryState(mediaBottomSheetState = aMediaDetailsBottomSheetState()),
|
||||
aMediaGalleryState(mediaBottomSheetState = aMediaBottomSheetStateDetails()),
|
||||
aMediaGalleryState(
|
||||
groupedMediaItems = AsyncData.Failure(Exception("Failed to load media")),
|
||||
),
|
||||
|
||||
+26
-23
@@ -132,7 +132,7 @@ fun MediaGalleryView(
|
||||
index = mode.ordinal,
|
||||
count = MediaGalleryMode.entries.size,
|
||||
selected = state.mode == mode,
|
||||
onClick = { state.eventSink(MediaGalleryEvents.ChangeMode(mode)) },
|
||||
onClick = { state.eventSink(MediaGalleryEvent.ChangeMode(mode)) },
|
||||
text = stringResource(mode.stringResource),
|
||||
)
|
||||
}
|
||||
@@ -158,24 +158,27 @@ fun MediaGalleryView(
|
||||
}
|
||||
when (val bottomSheetState = state.mediaBottomSheetState) {
|
||||
MediaBottomSheetState.Hidden -> Unit
|
||||
is MediaBottomSheetState.MediaDetailsBottomSheetState -> {
|
||||
is MediaBottomSheetState.Details -> {
|
||||
MediaDetailsBottomSheet(
|
||||
state = bottomSheetState,
|
||||
onViewInTimeline = { eventId ->
|
||||
state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId))
|
||||
state.eventSink(MediaGalleryEvent.ViewInTimeline(eventId))
|
||||
},
|
||||
onShare = { eventId ->
|
||||
state.eventSink(MediaGalleryEvents.Share(eventId))
|
||||
state.eventSink(MediaGalleryEvent.Share(eventId))
|
||||
},
|
||||
onForward = { eventId ->
|
||||
state.eventSink(MediaGalleryEvents.Forward(eventId))
|
||||
state.eventSink(MediaGalleryEvent.Forward(eventId))
|
||||
},
|
||||
onDownload = { eventId ->
|
||||
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
|
||||
state.eventSink(MediaGalleryEvent.SaveOnDisk(eventId))
|
||||
},
|
||||
onOpenWith = { eventId ->
|
||||
state.eventSink(MediaGalleryEvent.OpenWith(eventId))
|
||||
},
|
||||
onDelete = { eventId ->
|
||||
state.eventSink(
|
||||
MediaGalleryEvents.ConfirmDelete(
|
||||
MediaGalleryEvent.ConfirmDelete(
|
||||
eventId = eventId,
|
||||
mediaInfo = bottomSheetState.mediaInfo,
|
||||
thumbnailSource = bottomSheetState.thumbnailSource,
|
||||
@@ -183,18 +186,18 @@ fun MediaGalleryView(
|
||||
)
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(MediaGalleryEvents.CloseBottomSheet)
|
||||
state.eventSink(MediaGalleryEvent.CloseBottomSheet)
|
||||
},
|
||||
)
|
||||
}
|
||||
is MediaBottomSheetState.MediaDeleteConfirmationState -> {
|
||||
is MediaBottomSheetState.DeleteConfirmation -> {
|
||||
MediaDeleteConfirmationBottomSheet(
|
||||
state = bottomSheetState,
|
||||
onDelete = {
|
||||
state.eventSink(MediaGalleryEvents.Delete(it))
|
||||
state.eventSink(MediaGalleryEvent.Delete(it))
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(MediaGalleryEvents.CloseBottomSheet)
|
||||
state.eventSink(MediaGalleryEvent.CloseBottomSheet)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -213,7 +216,7 @@ private fun MediaGalleryPage(
|
||||
val loadingItem = groupedMediaItems.dataOrNull()?.getItems(mode)?.singleOrNull() as? MediaItem.LoadingIndicator
|
||||
if (loadingItem != null) {
|
||||
LaunchedEffect(loadingItem.timestamp) {
|
||||
state.eventSink(MediaGalleryEvents.LoadMore(loadingItem.direction))
|
||||
state.eventSink(MediaGalleryEvent.LoadMore(loadingItem.direction))
|
||||
}
|
||||
}
|
||||
LoadingContent(mode)
|
||||
@@ -258,7 +261,7 @@ private fun AsyncData<GroupedMediaItems>.isLoadingItems(mode: MediaGalleryMode):
|
||||
@Composable
|
||||
private fun MediaGalleryImages(
|
||||
imagesAndVideos: ImmutableList<MediaItem>,
|
||||
eventSink: (MediaGalleryEvents) -> Unit,
|
||||
eventSink: (MediaGalleryEvent) -> Unit,
|
||||
onItemClick: (MediaItem.Event) -> Unit,
|
||||
) {
|
||||
if (imagesAndVideos.isEmpty()) {
|
||||
@@ -279,7 +282,7 @@ private fun MediaGalleryImages(
|
||||
@Composable
|
||||
private fun MediaGalleryFiles(
|
||||
files: ImmutableList<MediaItem>,
|
||||
eventSink: (MediaGalleryEvents) -> Unit,
|
||||
eventSink: (MediaGalleryEvent) -> Unit,
|
||||
onItemClick: (MediaItem.Event) -> Unit,
|
||||
) {
|
||||
if (files.isEmpty()) {
|
||||
@@ -300,7 +303,7 @@ private fun MediaGalleryFiles(
|
||||
@Composable
|
||||
private fun MediaGalleryFilesList(
|
||||
files: ImmutableList<MediaItem>,
|
||||
eventSink: (MediaGalleryEvents) -> Unit,
|
||||
eventSink: (MediaGalleryEvent) -> Unit,
|
||||
onItemClick: (MediaItem.Event) -> Unit,
|
||||
) {
|
||||
val presenterFactories = LocalMediaItemPresenterFactories.current
|
||||
@@ -318,7 +321,7 @@ private fun MediaGalleryFilesList(
|
||||
file = item,
|
||||
onClick = { onItemClick(item) },
|
||||
onLongClick = {
|
||||
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
},
|
||||
)
|
||||
is MediaItem.Audio -> AudioItemView(
|
||||
@@ -326,7 +329,7 @@ private fun MediaGalleryFilesList(
|
||||
audio = item,
|
||||
onClick = { onItemClick(item) },
|
||||
onLongClick = {
|
||||
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
},
|
||||
)
|
||||
is MediaItem.Voice -> {
|
||||
@@ -336,7 +339,7 @@ private fun MediaGalleryFilesList(
|
||||
state = presenter.present(),
|
||||
voice = item,
|
||||
onLongClick = {
|
||||
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -361,7 +364,7 @@ private fun MediaGalleryFilesList(
|
||||
@Composable
|
||||
private fun MediaGalleryImageGrid(
|
||||
imagesAndVideos: ImmutableList<MediaItem>,
|
||||
eventSink: (MediaGalleryEvents) -> Unit,
|
||||
eventSink: (MediaGalleryEvent) -> Unit,
|
||||
onItemClick: (MediaItem.Event) -> Unit,
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
@@ -403,7 +406,7 @@ private fun MediaGalleryImageGrid(
|
||||
image = item,
|
||||
onClick = { onItemClick(item) },
|
||||
onLongClick = {
|
||||
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
},
|
||||
)
|
||||
is MediaItem.Video -> VideoItemView(
|
||||
@@ -411,7 +414,7 @@ private fun MediaGalleryImageGrid(
|
||||
video = item,
|
||||
onClick = { onItemClick(item) },
|
||||
onLongClick = {
|
||||
eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
},
|
||||
)
|
||||
is MediaItem.LoadingIndicator -> LoadingMoreIndicator(
|
||||
@@ -427,7 +430,7 @@ private fun MediaGalleryImageGrid(
|
||||
@Composable
|
||||
private fun LoadingMoreIndicator(
|
||||
item: MediaItem.LoadingIndicator,
|
||||
eventSink: (MediaGalleryEvents) -> Unit,
|
||||
eventSink: (MediaGalleryEvent) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
@@ -452,7 +455,7 @@ private fun LoadingMoreIndicator(
|
||||
}
|
||||
val latestEventSink by rememberUpdatedState(eventSink)
|
||||
LaunchedEffect(item.timestamp) {
|
||||
latestEventSink(MediaGalleryEvents.LoadMore(item.direction))
|
||||
latestEventSink(MediaGalleryEvent.LoadMore(item.direction))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -9,6 +9,7 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.local.audio
|
||||
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import io.element.android.libraries.ui.strings.Strings
|
||||
|
||||
fun MediaMetadata?.hasArtwork(): Boolean {
|
||||
return this?.artworkData != null || this?.artworkUri != null
|
||||
@@ -22,13 +23,13 @@ fun MediaMetadata?.buildInfo(): String {
|
||||
}
|
||||
if (title != null) {
|
||||
if (isNotEmpty()) {
|
||||
append(" - ")
|
||||
append(Strings.NICE_SEPARATOR)
|
||||
}
|
||||
append(title)
|
||||
}
|
||||
if (recordingYear != null) {
|
||||
if (isNotEmpty()) {
|
||||
append(" - ")
|
||||
append(Strings.NICE_SEPARATOR)
|
||||
}
|
||||
append(recordingYear)
|
||||
}
|
||||
|
||||
+14
-14
@@ -11,22 +11,22 @@ package io.element.android.libraries.mediaviewer.impl.viewer
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
sealed interface MediaViewerEvents {
|
||||
data class LoadMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
||||
data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents
|
||||
data class Share(val data: MediaViewerPageData.MediaViewerData) : 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
|
||||
sealed interface MediaViewerEvent {
|
||||
data class LoadMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||
data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||
data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||
data class OpenWith(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||
data class ClearLoadingError(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||
data class ViewInTimeline(val eventId: EventId) : MediaViewerEvent
|
||||
data class Forward(val eventId: EventId) : MediaViewerEvent
|
||||
data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||
data class ConfirmDelete(
|
||||
val eventId: EventId,
|
||||
val data: MediaViewerPageData.MediaViewerData,
|
||||
) : MediaViewerEvents
|
||||
) : MediaViewerEvent
|
||||
|
||||
data object CloseBottomSheet : MediaViewerEvents
|
||||
data class Delete(val eventId: EventId) : MediaViewerEvents
|
||||
data class OnNavigateTo(val index: Int) : MediaViewerEvents
|
||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvents
|
||||
data object CloseBottomSheet : MediaViewerEvent
|
||||
data class Delete(val eventId: EventId) : MediaViewerEvent
|
||||
data class OnNavigateTo(val index: Int) : MediaViewerEvent
|
||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent
|
||||
}
|
||||
+16
-16
@@ -95,43 +95,43 @@ class MediaViewerPresenter(
|
||||
}
|
||||
localMediaActions.Configure()
|
||||
|
||||
fun handleEvent(event: MediaViewerEvents) {
|
||||
fun handleEvent(event: MediaViewerEvent) {
|
||||
when (event) {
|
||||
is MediaViewerEvents.LoadMedia -> {
|
||||
is MediaViewerEvent.LoadMedia -> {
|
||||
coroutineScope.downloadMedia(data = event.data)
|
||||
}
|
||||
is MediaViewerEvents.ClearLoadingError -> {
|
||||
is MediaViewerEvent.ClearLoadingError -> {
|
||||
dataSource.clearLoadingError(event.data)
|
||||
}
|
||||
is MediaViewerEvents.SaveOnDisk -> {
|
||||
is MediaViewerEvent.SaveOnDisk -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.saveOnDisk(event.data.downloadedMedia.value)
|
||||
}
|
||||
is MediaViewerEvents.Share -> {
|
||||
is MediaViewerEvent.Share -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.share(event.data.downloadedMedia.value)
|
||||
}
|
||||
is MediaViewerEvents.OpenWith -> {
|
||||
is MediaViewerEvent.OpenWith -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.open(event.data.downloadedMedia.value)
|
||||
}
|
||||
is MediaViewerEvents.Delete -> {
|
||||
is MediaViewerEvent.Delete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
coroutineScope.delete(event.eventId)
|
||||
}
|
||||
is MediaViewerEvents.ViewInTimeline -> {
|
||||
is MediaViewerEvent.ViewInTimeline -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
navigator.onViewInTimelineClick(event.eventId)
|
||||
}
|
||||
is MediaViewerEvents.Forward -> {
|
||||
is MediaViewerEvent.Forward -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
navigator.onForwardClick(
|
||||
eventId = event.eventId,
|
||||
fromPinnedEvents = inputs.mode.getTimelineMode() == Timeline.Mode.PinnedEvents,
|
||||
)
|
||||
}
|
||||
is MediaViewerEvents.OpenInfo -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
is MediaViewerEvent.OpenInfo -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Details(
|
||||
eventId = event.data.eventId,
|
||||
canDelete = when (event.data.mediaInfo.senderId) {
|
||||
null -> false
|
||||
@@ -142,20 +142,20 @@ class MediaViewerPresenter(
|
||||
thumbnailSource = event.data.thumbnailSource,
|
||||
)
|
||||
}
|
||||
is MediaViewerEvents.ConfirmDelete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState(
|
||||
is MediaViewerEvent.ConfirmDelete -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.DeleteConfirmation(
|
||||
eventId = event.eventId,
|
||||
mediaInfo = event.data.mediaInfo,
|
||||
thumbnailSource = event.data.thumbnailSource ?: event.data.mediaSource,
|
||||
)
|
||||
}
|
||||
MediaViewerEvents.CloseBottomSheet -> {
|
||||
MediaViewerEvent.CloseBottomSheet -> {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
}
|
||||
is MediaViewerEvents.OnNavigateTo -> {
|
||||
is MediaViewerEvent.OnNavigateTo -> {
|
||||
currentIndex.intValue = event.index
|
||||
}
|
||||
is MediaViewerEvents.LoadMore -> coroutineScope.launch {
|
||||
is MediaViewerEvent.LoadMore -> coroutineScope.launch {
|
||||
dataSource.loadMore(event.direction)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ data class MediaViewerState(
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val canShowInfo: Boolean,
|
||||
val mediaBottomSheetState: MediaBottomSheetState,
|
||||
val eventSink: (MediaViewerEvents) -> Unit,
|
||||
val eventSink: (MediaViewerEvent) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface MediaViewerPageData {
|
||||
|
||||
+5
-5
@@ -25,8 +25,8 @@ import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||
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.MediaBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDeleteConfirmation
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
private const val LONG_CAPTION = "This is a very long caption that should be scrollable in the media viewer. " +
|
||||
@@ -141,10 +141,10 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||
)
|
||||
},
|
||||
aMediaViewerState(
|
||||
mediaBottomSheetState = aMediaDetailsBottomSheetState(),
|
||||
mediaBottomSheetState = aMediaBottomSheetStateDetails(),
|
||||
),
|
||||
aMediaViewerState(
|
||||
mediaBottomSheetState = aMediaDeleteConfirmationState(),
|
||||
mediaBottomSheetState = aMediaBottomSheetStateDeleteConfirmation(),
|
||||
),
|
||||
anAudioMediaInfo(
|
||||
waveForm = WaveFormSamples.realisticWaveForm,
|
||||
@@ -226,7 +226,7 @@ fun aMediaViewerState(
|
||||
currentIndex: Int = 0,
|
||||
canShowInfo: Boolean = true,
|
||||
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
||||
eventSink: (MediaViewerEvents) -> Unit = {},
|
||||
eventSink: (MediaViewerEvent) -> Unit = {},
|
||||
) = MediaViewerState(
|
||||
initiallySelectedEventId = EventId("\$a:b"),
|
||||
listData = listData.toImmutableList(),
|
||||
|
||||
+47
-35
@@ -65,7 +65,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.audio.api.AudioFocus
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
@@ -85,7 +84,6 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.impl.R
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
|
||||
@@ -101,6 +99,9 @@ import me.saket.telephoto.zoomable.rememberZoomableState
|
||||
|
||||
val topAppBarHeight = 88.dp
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623
|
||||
*/
|
||||
@Composable
|
||||
fun MediaViewerView(
|
||||
state: MediaViewerState,
|
||||
@@ -126,7 +127,7 @@ fun MediaViewerView(
|
||||
}
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||
state.eventSink(MediaViewerEvents.OnNavigateTo(page))
|
||||
state.eventSink(MediaViewerEvent.OnNavigateTo(page))
|
||||
}
|
||||
}
|
||||
HorizontalPager(
|
||||
@@ -145,7 +146,7 @@ fun MediaViewerView(
|
||||
}
|
||||
is MediaViewerPageData.Loading -> {
|
||||
LaunchedEffect(dataForPage.timestamp) {
|
||||
state.eventSink(MediaViewerEvents.LoadMore(dataForPage.direction))
|
||||
state.eventSink(MediaViewerEvent.LoadMore(dataForPage.direction))
|
||||
}
|
||||
MediaViewerLoadingPage(
|
||||
onDismiss = onBackClick,
|
||||
@@ -154,7 +155,7 @@ fun MediaViewerView(
|
||||
is MediaViewerPageData.MediaViewerData -> {
|
||||
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
||||
LaunchedEffect(Unit) {
|
||||
state.eventSink(MediaViewerEvents.LoadMedia(dataForPage))
|
||||
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -173,10 +174,10 @@ fun MediaViewerView(
|
||||
textFileViewer = textFileViewer,
|
||||
onDismiss = onBackClick,
|
||||
onRetry = {
|
||||
state.eventSink(MediaViewerEvents.LoadMedia(dataForPage))
|
||||
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
|
||||
},
|
||||
onDismissError = {
|
||||
state.eventSink(MediaViewerEvents.ClearLoadingError(dataForPage))
|
||||
state.eventSink(MediaViewerEvent.ClearLoadingError(dataForPage))
|
||||
},
|
||||
onShowOverlayChange = {
|
||||
showOverlay = it
|
||||
@@ -214,10 +215,15 @@ fun MediaViewerView(
|
||||
data = currentData,
|
||||
canShowInfo = state.canShowInfo,
|
||||
onBackClick = onBackClick,
|
||||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvents.OpenInfo(currentData))
|
||||
onShareClick = {
|
||||
state.eventSink(MediaViewerEvent.Share(currentData))
|
||||
},
|
||||
onSaveClick = {
|
||||
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
|
||||
},
|
||||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
|
||||
},
|
||||
eventSink = state.eventSink
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
@@ -247,29 +253,34 @@ fun MediaViewerView(
|
||||
|
||||
when (val bottomSheetState = state.mediaBottomSheetState) {
|
||||
MediaBottomSheetState.Hidden -> Unit
|
||||
is MediaBottomSheetState.MediaDetailsBottomSheetState -> {
|
||||
is MediaBottomSheetState.Details -> {
|
||||
MediaDetailsBottomSheet(
|
||||
state = bottomSheetState,
|
||||
onViewInTimeline = {
|
||||
state.eventSink(MediaViewerEvents.ViewInTimeline(it))
|
||||
state.eventSink(MediaViewerEvent.ViewInTimeline(it))
|
||||
},
|
||||
onShare = {
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(MediaViewerEvents.Share(currentData))
|
||||
state.eventSink(MediaViewerEvent.Share(currentData))
|
||||
}
|
||||
},
|
||||
onForward = {
|
||||
state.eventSink(MediaViewerEvents.Forward(it))
|
||||
state.eventSink(MediaViewerEvent.Forward(it))
|
||||
},
|
||||
onDownload = {
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk(currentData))
|
||||
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
|
||||
}
|
||||
},
|
||||
onOpenWith = {
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(MediaViewerEvent.OpenWith(currentData))
|
||||
}
|
||||
},
|
||||
onDelete = { eventId ->
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(
|
||||
MediaViewerEvents.ConfirmDelete(
|
||||
MediaViewerEvent.ConfirmDelete(
|
||||
eventId,
|
||||
currentData,
|
||||
)
|
||||
@@ -277,18 +288,18 @@ fun MediaViewerView(
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(MediaViewerEvents.CloseBottomSheet)
|
||||
state.eventSink(MediaViewerEvent.CloseBottomSheet)
|
||||
},
|
||||
)
|
||||
}
|
||||
is MediaBottomSheetState.MediaDeleteConfirmationState -> {
|
||||
is MediaBottomSheetState.DeleteConfirmation -> {
|
||||
MediaDeleteConfirmationBottomSheet(
|
||||
state = bottomSheetState,
|
||||
onDelete = {
|
||||
state.eventSink(MediaViewerEvents.Delete(it))
|
||||
state.eventSink(MediaViewerEvent.Delete(it))
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(MediaViewerEvents.CloseBottomSheet)
|
||||
state.eventSink(MediaViewerEvent.CloseBottomSheet)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -457,12 +468,12 @@ private fun MediaViewerTopBar(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
canShowInfo: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onShareClick: () -> Unit,
|
||||
onSaveClick: () -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
eventSink: (MediaViewerEvents) -> Unit,
|
||||
) {
|
||||
val downloadedMedia by data.downloadedMedia
|
||||
val actionsEnabled = downloadedMedia.isSuccess()
|
||||
val mimeType = data.mediaInfo.mimeType
|
||||
val senderName = data.mediaInfo.senderName
|
||||
val dateSent = data.mediaInfo.dateSent
|
||||
TopAppBar(
|
||||
@@ -498,21 +509,22 @@ private fun MediaViewerTopBar(
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onShareClick,
|
||||
enabled = actionsEnabled,
|
||||
onClick = {
|
||||
eventSink(MediaViewerEvents.OpenWith(data))
|
||||
},
|
||||
) {
|
||||
when (mimeType) {
|
||||
MimeTypes.Apk -> Icon(
|
||||
resourceId = R.drawable.ic_apk_install,
|
||||
contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
|
||||
)
|
||||
else -> Icon(
|
||||
imageVector = CompoundIcons.PopOut(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_open_with)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = CompoundIcons.ShareAndroid(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_share),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onSaveClick,
|
||||
enabled = actionsEnabled,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Download(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_download),
|
||||
)
|
||||
}
|
||||
if (canShowInfo) {
|
||||
IconButton(
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
<string name="screen_media_browser_media_empty_state_subtitle">"Images and videos uploaded to this room will be shown here."</string>
|
||||
<string name="screen_media_browser_media_empty_state_title">"No media uploaded yet"</string>
|
||||
<string name="screen_media_browser_title">"Media and files"</string>
|
||||
<string name="screen_media_details_file_format">"File format"</string>
|
||||
<string name="screen_media_details_filename">"File name"</string>
|
||||
<string name="screen_media_details_file_format">"Format"</string>
|
||||
<string name="screen_media_details_filename">"Name"</string>
|
||||
<string name="screen_media_details_no_more_files_to_show">"No more files to show"</string>
|
||||
<string name="screen_media_details_no_more_media_to_show">"No more media to show"</string>
|
||||
<string name="screen_media_details_title">"File info"</string>
|
||||
<string name="screen_media_details_uploaded_by">"Uploaded by"</string>
|
||||
<string name="screen_media_details_uploaded_on">"Uploaded on"</string>
|
||||
</resources>
|
||||
|
||||
+3
-3
@@ -33,7 +33,7 @@ class MediaDeleteConfirmationBottomSheetTest {
|
||||
|
||||
@Test
|
||||
fun `clicking on Cancel invokes expected callback`() {
|
||||
val state = aMediaDeleteConfirmationState()
|
||||
val state = aMediaBottomSheetStateDeleteConfirmation()
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMediaDeleteConfirmationBottomSheet(
|
||||
state = state,
|
||||
@@ -45,7 +45,7 @@ class MediaDeleteConfirmationBottomSheetTest {
|
||||
|
||||
@Test
|
||||
fun `clicking on Remove invokes expected callback`() {
|
||||
val state = aMediaDeleteConfirmationState()
|
||||
val state = aMediaBottomSheetStateDeleteConfirmation()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDeleteConfirmationBottomSheet(
|
||||
state = state,
|
||||
@@ -58,7 +58,7 @@ class MediaDeleteConfirmationBottomSheetTest {
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMediaDeleteConfirmationBottomSheet(
|
||||
state: MediaBottomSheetState.MediaDeleteConfirmationState,
|
||||
state: MediaBottomSheetState.DeleteConfirmation,
|
||||
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
||||
+14
-12
@@ -34,7 +34,7 @@ class MediaDetailsBottomSheetTest {
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on View in timeline invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
val state = aMediaBottomSheetStateDetails()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
@@ -47,7 +47,7 @@ class MediaDetailsBottomSheetTest {
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on Share invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
val state = aMediaBottomSheetStateDetails()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
@@ -60,7 +60,7 @@ class MediaDetailsBottomSheetTest {
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on Forward invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
val state = aMediaBottomSheetStateDetails()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
@@ -72,35 +72,35 @@ class MediaDetailsBottomSheetTest {
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on Save invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
fun `clicking on Download invokes expected callback`() {
|
||||
val state = aMediaBottomSheetStateDetails()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onDownload = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
rule.clickOn(CommonStrings.action_download)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on Remove invokes expected callback`() {
|
||||
val state = aMediaDetailsBottomSheetState()
|
||||
fun `clicking on Delete invokes expected callback`() {
|
||||
val state = aMediaBottomSheetStateDetails()
|
||||
ensureCalledOnceWithParam(state.eventId) { callback ->
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
state = state,
|
||||
onDelete = callback,
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
|
||||
rule.clickOn(CommonStrings.action_remove)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_delete)).assertExists()
|
||||
rule.clickOn(CommonStrings.action_delete)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `Remove is not present if canDelete is false`() {
|
||||
val state = aMediaDetailsBottomSheetState(
|
||||
val state = aMediaBottomSheetStateDetails(
|
||||
canDelete = false,
|
||||
)
|
||||
rule.setMediaDetailsBottomSheet(
|
||||
@@ -111,11 +111,12 @@ class MediaDetailsBottomSheetTest {
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMediaDetailsBottomSheet(
|
||||
state: MediaBottomSheetState.MediaDetailsBottomSheetState,
|
||||
state: MediaBottomSheetState.Details,
|
||||
onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onForward: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOpenWith: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
@@ -126,6 +127,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMedia
|
||||
onShare = onShare,
|
||||
onForward = onForward,
|
||||
onDownload = onDownload,
|
||||
onOpenWith = onOpenWith,
|
||||
onDelete = onDelete,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
|
||||
+102
-26
@@ -6,6 +6,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery
|
||||
|
||||
import android.net.Uri
|
||||
@@ -27,6 +29,7 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource
|
||||
import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||
@@ -39,6 +42,8 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -52,8 +57,12 @@ class MediaGalleryPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val configureLambda = lambdaRecorder<Unit> { }
|
||||
val startLambda = lambdaRecorder<Unit> { }
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
localMediaActions = FakeLocalMediaActions(
|
||||
configureResult = configureLambda,
|
||||
),
|
||||
mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = startLambda,
|
||||
),
|
||||
@@ -70,6 +79,7 @@ class MediaGalleryPresenterTest {
|
||||
assertThat(initialState.groupedMediaItems.isUninitialized()).isTrue()
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
}
|
||||
configureLambda.assertions().isCalledOnce()
|
||||
startLambda.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@@ -84,10 +94,10 @@ class MediaGalleryPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
|
||||
initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files))
|
||||
initialState.eventSink(MediaGalleryEvent.ChangeMode(MediaGalleryMode.Files))
|
||||
val state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(MediaGalleryMode.Files)
|
||||
state.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Images))
|
||||
state.eventSink(MediaGalleryEvent.ChangeMode(MediaGalleryMode.Images))
|
||||
val imageModeState = awaitItem()
|
||||
assertThat(imageModeState.mode).isEqualTo(MediaGalleryMode.Images)
|
||||
}
|
||||
@@ -123,10 +133,10 @@ class MediaGalleryPresenterTest {
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
)
|
||||
initialState.eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
initialState.eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
val state = awaitItem()
|
||||
assertThat(state.mediaBottomSheetState).isEqualTo(
|
||||
MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
MediaBottomSheetState.Details(
|
||||
eventId = AN_EVENT_ID,
|
||||
canDelete = canDeleteOwn,
|
||||
mediaInfo = item.mediaInfo,
|
||||
@@ -134,7 +144,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
)
|
||||
// Close the bottom sheet
|
||||
state.eventSink(MediaGalleryEvents.CloseBottomSheet)
|
||||
state.eventSink(MediaGalleryEvent.CloseBottomSheet)
|
||||
val closedState = awaitItem()
|
||||
assertThat(closedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
@@ -170,10 +180,10 @@ class MediaGalleryPresenterTest {
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID_2,
|
||||
)
|
||||
initialState.eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
initialState.eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
val state = awaitItem()
|
||||
assertThat(state.mediaBottomSheetState).isEqualTo(
|
||||
MediaBottomSheetState.MediaDetailsBottomSheetState(
|
||||
MediaBottomSheetState.Details(
|
||||
eventId = AN_EVENT_ID,
|
||||
canDelete = canDeleteOther,
|
||||
mediaInfo = item.mediaInfo,
|
||||
@@ -181,7 +191,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
)
|
||||
// Close the bottom sheet
|
||||
state.eventSink(MediaGalleryEvents.CloseBottomSheet)
|
||||
state.eventSink(MediaGalleryEvent.CloseBottomSheet)
|
||||
val closedState = awaitItem()
|
||||
assertThat(closedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
@@ -199,17 +209,17 @@ class MediaGalleryPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
// Delete bottom sheet
|
||||
val item = aMediaItemImage()
|
||||
initialState.eventSink(MediaGalleryEvents.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource))
|
||||
initialState.eventSink(MediaGalleryEvent.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource))
|
||||
val deleteState = awaitItem()
|
||||
assertThat(deleteState.mediaBottomSheetState).isEqualTo(
|
||||
MediaBottomSheetState.MediaDeleteConfirmationState(
|
||||
MediaBottomSheetState.DeleteConfirmation(
|
||||
eventId = AN_EVENT_ID,
|
||||
mediaInfo = item.mediaInfo,
|
||||
thumbnailSource = item.thumbnailSource,
|
||||
)
|
||||
)
|
||||
// Close the bottom sheet
|
||||
deleteState.eventSink(MediaGalleryEvents.CloseBottomSheet)
|
||||
deleteState.eventSink(MediaGalleryEvent.CloseBottomSheet)
|
||||
val deleteClosedState = awaitItem()
|
||||
assertThat(deleteClosedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
}
|
||||
@@ -226,7 +236,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Delete(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.Delete(AN_EVENT_ID))
|
||||
deleteItemLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
@@ -236,7 +246,7 @@ class MediaGalleryPresenterTest {
|
||||
val presenter = createMediaGalleryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.Share(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +268,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.Share(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage).isNull()
|
||||
}
|
||||
@@ -283,7 +293,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.Share(AN_EVENT_ID))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java)
|
||||
@@ -295,7 +305,7 @@ class MediaGalleryPresenterTest {
|
||||
val presenter = createMediaGalleryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.SaveOnDisk(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,23 +314,89 @@ class MediaGalleryPresenterTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val saveOnDiskResult = lambdaRecorder<LocalMedia, Result<Unit>> { _ -> Result.success(Unit) }
|
||||
val media = aMediaItemImage(eventId = AN_EVENT_ID)
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
aGroupedMediaItems(
|
||||
imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)),
|
||||
imageAndVideoItems = listOf(media),
|
||||
fileItems = emptyList(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
localMediaActions = FakeLocalMediaActions(
|
||||
saveOnDiskResult = saveOnDiskResult,
|
||||
),
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.SaveOnDisk(AN_EVENT_ID))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_file_saved_on_disk_android)
|
||||
saveOnDiskResult.assertions().isCalledOnce().with(
|
||||
value(
|
||||
LocalMedia(
|
||||
uri = mockMediaUri,
|
||||
info = media.mediaInfo,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - open with closes the bottom sheet and invokes the navigator`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
val openWithResult = lambdaRecorder<LocalMedia, Result<Unit>> { _ -> Result.success(Unit) }
|
||||
val item = aMediaItemImage(
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
)
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
aGroupedMediaItems(
|
||||
imageAndVideoItems = listOf(item),
|
||||
fileItems = emptyList(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
localMediaActions = FakeLocalMediaActions(
|
||||
openResult = openWithResult,
|
||||
),
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
room = FakeJoinedRoom(
|
||||
createTimelineResult = { Result.success(FakeTimeline()) },
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = FakeRoomPermissions(
|
||||
canRedactOwn = true
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
withBottomSheetState.eventSink(MediaGalleryEvent.OpenWith(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
advanceUntilIdle()
|
||||
openWithResult.assertions().isCalledOnce().with(
|
||||
value(
|
||||
LocalMedia(
|
||||
uri = mockMediaUri,
|
||||
info = item.mediaInfo,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +419,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
initialState.eventSink(MediaGalleryEvent.SaveOnDisk(AN_EVENT_ID))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java)
|
||||
@@ -373,10 +449,10 @@ class MediaGalleryPresenterTest {
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
)
|
||||
initialState.eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
initialState.eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
withBottomSheetState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID))
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
withBottomSheetState.eventSink(MediaGalleryEvent.ViewInTimeline(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
@@ -406,10 +482,10 @@ class MediaGalleryPresenterTest {
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID,
|
||||
)
|
||||
initialState.eventSink(MediaGalleryEvents.OpenInfo(item))
|
||||
initialState.eventSink(MediaGalleryEvent.OpenInfo(item))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
withBottomSheetState.eventSink(MediaGalleryEvents.Forward(AN_EVENT_ID))
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
withBottomSheetState.eventSink(MediaGalleryEvent.Forward(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onForwardClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
@@ -427,7 +503,7 @@ class MediaGalleryPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||
initialState.eventSink(MediaGalleryEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||
loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS))
|
||||
}
|
||||
}
|
||||
|
||||
+28
-28
@@ -226,7 +226,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.LoadMedia(
|
||||
MediaViewerEvent.LoadMedia(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
@@ -266,16 +266,16 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OpenInfo(
|
||||
MediaViewerEvent.OpenInfo(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
val withInfoState = awaitItem()
|
||||
assertThat(withInfoState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
assertThat(withInfoState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
withInfoState.eventSink(
|
||||
MediaViewerEvents.CloseBottomSheet
|
||||
MediaViewerEvent.CloseBottomSheet
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
@@ -306,7 +306,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.ClearLoadingError(
|
||||
MediaViewerEvent.ClearLoadingError(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
@@ -339,7 +339,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.Share(
|
||||
MediaViewerEvent.Share(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
@@ -372,7 +372,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.SaveOnDisk(
|
||||
MediaViewerEvent.SaveOnDisk(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
@@ -405,7 +405,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OpenWith(
|
||||
MediaViewerEvent.OpenWith(
|
||||
aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
)
|
||||
@@ -438,7 +438,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.ConfirmDelete(
|
||||
MediaViewerEvent.ConfirmDelete(
|
||||
eventId = AN_EVENT_ID,
|
||||
data = aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
@@ -446,9 +446,9 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
)
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDeleteConfirmationState::class.java)
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.DeleteConfirmation::class.java)
|
||||
withBottomSheetState.eventSink(
|
||||
MediaViewerEvents.CloseBottomSheet
|
||||
MediaViewerEvent.CloseBottomSheet
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
@@ -498,7 +498,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.ConfirmDelete(
|
||||
MediaViewerEvent.ConfirmDelete(
|
||||
eventId = AN_EVENT_ID,
|
||||
data = aMediaViewerPageData(
|
||||
mediaSource = MediaSource(aUrl)
|
||||
@@ -506,9 +506,9 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
)
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDeleteConfirmationState::class.java)
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.DeleteConfirmation::class.java)
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.Delete(
|
||||
MediaViewerEvent.Delete(
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
)
|
||||
@@ -551,7 +551,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(1)
|
||||
MediaViewerEvent.OnNavigateTo(1)
|
||||
)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.currentIndex).isEqualTo(1)
|
||||
@@ -606,7 +606,7 @@ class MediaViewerPresenterTest {
|
||||
val updatedState = awaitItem()
|
||||
// User navigate to the last item (forward loading indicator)
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(2)
|
||||
MediaViewerEvent.OnNavigateTo(2)
|
||||
)
|
||||
// data source claims that there is no more items to load forward
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
@@ -680,7 +680,7 @@ class MediaViewerPresenterTest {
|
||||
val updatedState = awaitItem()
|
||||
// User navigate to the first item (backward loading indicator)
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(0)
|
||||
MediaViewerEvent.OnNavigateTo(0)
|
||||
)
|
||||
// data source claims that there is no more items to load backward
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
@@ -728,7 +728,7 @@ class MediaViewerPresenterTest {
|
||||
val updatedState = awaitItem()
|
||||
// User navigate to the media
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(1)
|
||||
MediaViewerEvent.OnNavigateTo(1)
|
||||
)
|
||||
skipItems(1)
|
||||
// data source claims that there is no more items to load at all
|
||||
@@ -771,7 +771,7 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)
|
||||
MediaViewerEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS)
|
||||
)
|
||||
loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS))
|
||||
}
|
||||
@@ -796,10 +796,10 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData()))
|
||||
initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData()))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
initialState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID))
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
initialState.eventSink(MediaViewerEvent.ViewInTimeline(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
|
||||
@@ -825,10 +825,10 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData()))
|
||||
initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData()))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID))
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
initialState.eventSink(MediaViewerEvent.Forward(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onForwardClickLambda.assertions().isCalledOnce()
|
||||
@@ -856,10 +856,10 @@ class MediaViewerPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData()))
|
||||
initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData()))
|
||||
val withBottomSheetState = awaitItem()
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java)
|
||||
initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID))
|
||||
assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java)
|
||||
initialState.eventSink(MediaViewerEvent.Forward(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
|
||||
onForwardClickLambda.assertions().isCalledOnce()
|
||||
|
||||
+74
-50
@@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
@@ -19,7 +20,7 @@ import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeDown
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails
|
||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
@@ -43,7 +44,7 @@ class MediaViewerViewTest {
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
val state = aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
@@ -56,42 +57,54 @@ class MediaViewerViewTest {
|
||||
}
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on open emit expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.action_open_with,
|
||||
MediaViewerEvents.OpenWith(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on info emit expected Event`() {
|
||||
fun `clicking on info emits expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.a11y_view_details,
|
||||
MediaViewerEvents.OpenInfo(data),
|
||||
MediaViewerEvent.OpenInfo(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on top action share emits expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.action_share,
|
||||
MediaViewerEvent.Share(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on top action download emits expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.action_download,
|
||||
MediaViewerEvent.SaveOnDisk(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testMenuAction(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
contentDescriptionRes: Int,
|
||||
expectedEvent: MediaViewerEvents,
|
||||
@StringRes contentDescriptionRes: Int,
|
||||
expectedEvent: MediaViewerEvent,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
listData = listOf(data),
|
||||
@@ -102,8 +115,8 @@ class MediaViewerViewTest {
|
||||
rule.onNodeWithContentDescription(contentDescription).performClick()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(data),
|
||||
expectedEvent,
|
||||
)
|
||||
)
|
||||
@@ -111,44 +124,55 @@ class MediaViewerViewTest {
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on save emit expected Event`() {
|
||||
fun `clicking on download emits expected Event`() {
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
CommonStrings.action_save,
|
||||
MediaViewerEvents.SaveOnDisk(data),
|
||||
CommonStrings.action_download,
|
||||
MediaViewerEvent.SaveOnDisk(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on share emit expected Event`() {
|
||||
fun `clicking on share emits expected Event`() {
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
CommonStrings.action_share,
|
||||
MediaViewerEvents.Share(data),
|
||||
MediaViewerEvent.Share(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on open in emits expected Event`() {
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
CommonStrings.action_open_with,
|
||||
MediaViewerEvent.OpenWith(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testBottomSheetAction(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
contentDescriptionRes: Int,
|
||||
expectedEvent: MediaViewerEvents,
|
||||
@StringRes textRes: Int,
|
||||
expectedEvent: MediaViewerEvent,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
rule.setMediaViewerView(
|
||||
aMediaViewerState(
|
||||
listData = listOf(data),
|
||||
mediaBottomSheetState = aMediaDetailsBottomSheetState(),
|
||||
mediaBottomSheetState = aMediaBottomSheetStateDetails(),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(contentDescriptionRes)
|
||||
rule.clickOn(textRes)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(data),
|
||||
expectedEvent,
|
||||
)
|
||||
)
|
||||
@@ -156,7 +180,7 @@ class MediaViewerViewTest {
|
||||
|
||||
@Test
|
||||
fun `clicking on image hides the overlay`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
val state = aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
@@ -164,7 +188,7 @@ class MediaViewerViewTest {
|
||||
state = state,
|
||||
)
|
||||
// Ensure that the action are visible
|
||||
val contentDescription = rule.activity.getString(CommonStrings.action_open_with)
|
||||
val contentDescription = rule.activity.getString(CommonStrings.action_share)
|
||||
rule.onNodeWithContentDescription(contentDescription)
|
||||
.assertExists()
|
||||
.assertHasClickAction()
|
||||
@@ -176,15 +200,15 @@ class MediaViewerViewTest {
|
||||
.assertDoesNotExist()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking swipe on the image invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
val state = aMediaViewerState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
@@ -199,15 +223,15 @@ class MediaViewerViewTest {
|
||||
}
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error case, click on retry emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
|
||||
)
|
||||
@@ -220,16 +244,16 @@ class MediaViewerViewTest {
|
||||
rule.clickOn(CommonStrings.action_retry)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(data),
|
||||
MediaViewerEvent.LoadMedia(data),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error case, click on cancel emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvents>()
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Failure(IllegalStateException("error")),
|
||||
)
|
||||
@@ -242,9 +266,9 @@ class MediaViewerViewTest {
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvents.OnNavigateTo(0),
|
||||
MediaViewerEvents.LoadMedia(data),
|
||||
MediaViewerEvents.ClearLoadingError(data)
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
MediaViewerEvent.LoadMedia(data),
|
||||
MediaViewerEvent.ClearLoadingError(data)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+11
-19
@@ -11,37 +11,29 @@ package io.element.android.libraries.mediaviewer.test
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeLocalMediaActions : LocalMediaActions {
|
||||
var shouldFail = false
|
||||
|
||||
class FakeLocalMediaActions(
|
||||
val configureResult: () -> Unit = { },
|
||||
val saveOnDiskResult: (LocalMedia) -> Result<Unit> = { lambdaError() },
|
||||
val shareResult: (LocalMedia) -> Result<Unit> = { lambdaError() },
|
||||
val openResult: (LocalMedia) -> Result<Unit> = { lambdaError() },
|
||||
) : LocalMediaActions {
|
||||
@Composable
|
||||
override fun Configure() {
|
||||
// NOOP
|
||||
configureResult()
|
||||
}
|
||||
|
||||
override suspend fun saveOnDisk(localMedia: LocalMedia): Result<Unit> = simulateLongTask {
|
||||
if (shouldFail) {
|
||||
Result.failure(RuntimeException())
|
||||
} else {
|
||||
Result.success(Unit)
|
||||
}
|
||||
saveOnDiskResult(localMedia)
|
||||
}
|
||||
|
||||
override suspend fun share(localMedia: LocalMedia): Result<Unit> = simulateLongTask {
|
||||
if (shouldFail) {
|
||||
Result.failure(RuntimeException())
|
||||
} else {
|
||||
Result.success(Unit)
|
||||
}
|
||||
shareResult(localMedia)
|
||||
}
|
||||
|
||||
override suspend fun open(localMedia: LocalMedia): Result<Unit> = simulateLongTask {
|
||||
if (shouldFail) {
|
||||
Result.failure(RuntimeException())
|
||||
} else {
|
||||
Result.success(Unit)
|
||||
}
|
||||
openResult(localMedia)
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.ui.strings
|
||||
|
||||
object Strings {
|
||||
const val NICE_SEPARATOR = " • "
|
||||
}
|
||||
@@ -96,6 +96,7 @@
|
||||
<string name="action_discard">"Discard"</string>
|
||||
<string name="action_dismiss">"Dismiss"</string>
|
||||
<string name="action_done">"Done"</string>
|
||||
<string name="action_download">"Download"</string>
|
||||
<string name="action_edit">"Edit"</string>
|
||||
<string name="action_edit_caption">"Edit caption"</string>
|
||||
<string name="action_edit_poll">"Edit poll"</string>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:684f94bece9c9cc117eef1cdad6fca99bb085158454a890592254f27c57e9b0e
|
||||
size 39715
|
||||
oid sha256:67be1e8e93d0c10366f4b37f14ca0f90c1f4ccd5da9024c9a396d3d46507e18c
|
||||
size 40339
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67be1e8e93d0c10366f4b37f14ca0f90c1f4ccd5da9024c9a396d3d46507e18c
|
||||
size 40339
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3f89a1039bc2d101af319c0b6259d8eebc9d313ae34947aedee525945be23799
|
||||
size 44604
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6144f221b9d11c70d15b54321bfbff3d1de1454e6c73be34d7b2e82bd1625a94
|
||||
size 30701
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:881a6235b66bd13269103bcc7070e389041652da49ecdba68c6caf151301b6c0
|
||||
size 38428
|
||||
oid sha256:a512016e96b933f6089e60b634fa77f140dfa7de50d3a8c1a62092ad552d7f2d
|
||||
size 39216
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a512016e96b933f6089e60b634fa77f140dfa7de50d3a8c1a62092ad552d7f2d
|
||||
size 39216
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8608e63ef9ff79fd698c202d60a8c73f8874f8610601a0ff6dc85663ed82c7da
|
||||
size 43537
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2898f00a828a69ce9cdd56994d7878f6aae8c5b2ea5d1150df57aa2ebd7e537b
|
||||
size 29233
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9dd2d35e1c0a3b3a9576e35b8a5ad4a3d08441b8257bb48edd0a0816c9f24c68
|
||||
size 39634
|
||||
oid sha256:3ddbddbf5303bb86f901cdd880ca2fde15c7c4af22dd7cf1a7903dcab5df25b3
|
||||
size 40254
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa2ca41cebe2e37843f74319a56f3c18103929f06765f0950cd1945440909e63
|
||||
size 38223
|
||||
oid sha256:89e17d0b8fdd749b6e97385e8369132d52b0e10adf7d23157c3bdbeb7721e694
|
||||
size 39007
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d6ab1331e65a4d02accb4d20c899a06c37a022d9f9080133799e30763969300
|
||||
size 26774
|
||||
oid sha256:e641d1d6604b6c5f489ab38438d9f3b8dcea9802113011200e9ea589c4e2dbee
|
||||
size 25310
|
||||
|
||||
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:52174b55b1737787260a454c59f243b0e3f6327ef5ec71464744def928d165d4
|
||||
size 206785
|
||||
oid sha256:041669475888f2f0c3b2d34502ae72098547340d8c2422ea2a674d38eb6a6241
|
||||
size 207620
|
||||
|
||||
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:062f8342a3a3715498ca34488c2d9ffe09c1e6dbbe04df09e88fd107a33b174a
|
||||
size 653228
|
||||
oid sha256:5aa3bca6cd248ac4725fb35aa11a465029dc534b8b167aedbf3e9bc240577e9c
|
||||
size 654171
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f9ee78e2a034f34d77a438d305a5cc63ea583df80f83a14b1d81fcf74ca93f4
|
||||
size 665416
|
||||
oid sha256:bd5b76aefb0fde0605556e78c7286bf8ee8dd465def0e3095ebc97ce3427eafa
|
||||
size 666239
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d1e91c66363a5af74d4345f7837db178410c078e390b0cf9296d0ba4b3dfd7cb
|
||||
size 207004
|
||||
oid sha256:c56b22d79924d1f463f01428d5be1c69c8068b94a953160fae59a9f6faa112ad
|
||||
size 206047
|
||||
|
||||
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f6c84fbd21949a3d9d355bfb627b8a70f65fc669cb6b6818a325fe4577351f9
|
||||
size 196092
|
||||
oid sha256:9e23aad00065523bf1ee0e26cd094eb6a9bdd73d65f0bd9cac62cb79e7876481
|
||||
size 196533
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26bd927acc578cb3585f6e101c066f0a6adb6a6e424dcd196388d5a838d8b22b
|
||||
size 196396
|
||||
oid sha256:9d98ceaeb669342160d04783f07fbb21929d8be46d30181b60e0f09a99abfe79
|
||||
size 197127
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2c4440f5ed01f2c1b405bb2d444c52455ea8ee1c094baf7d2f85bd9a9ff98b02
|
||||
size 210117
|
||||
oid sha256:e72ef42ad838837ce2427af5677931f5db68f876eccc297894c7a2cdd437cffa
|
||||
size 210729
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:11f140569061e21e6b546118b57340a0830779580498a31627acb2e6c7313471
|
||||
size 210490
|
||||
oid sha256:58a323c4f06745a877ea546870fbe69ec927aea50584001b3871cad833fca1d4
|
||||
size 211343
|
||||
|
||||
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa2ca41cebe2e37843f74319a56f3c18103929f06765f0950cd1945440909e63
|
||||
size 38223
|
||||
oid sha256:89e17d0b8fdd749b6e97385e8369132d52b0e10adf7d23157c3bdbeb7721e694
|
||||
size 39007
|
||||
|
||||
LFS
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c7c09c4ff8e66c23d3cdf5c79ffe2d3305d569e3aa1e239dfbb981537e467cd
|
||||
size 133920
|
||||
oid sha256:55671428c52d37c3f63f8b3410817d4e893e2f4327b31b227bc7b7d9ff84b884
|
||||
size 134680
|
||||
|
||||
LFS
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e353d24c2b9b49abaa11e064b1581215ad65154d0e67372fe24684ba2695a0e
|
||||
size 442063
|
||||
oid sha256:6bddbe3b0e66e0a2ce49b39321057c94427933de81663f680237a398e9929ba3
|
||||
size 442729
|
||||
|
||||
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eaf4fb4908967c6092de9be0d7ca33108f0f507605d7591395a08594763fb0fe
|
||||
size 396202
|
||||
oid sha256:3bd4a96daaa24c01b7d0007fdd14460dc80c65c13d0ccc7a887b04fd90e9fa99
|
||||
size 396805
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb6999796f9275adce6b5e05b362649cb738c759af649f7bbb9c0ff01548a579
|
||||
size 131776
|
||||
oid sha256:f7bee65567cced59131d471b5c3795347a2ef52cf64ff74b47f1c983a0b4a36f
|
||||
size 130718
|
||||
|
||||
LFS
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8273abc2f10e6c42a65fb6aef2e237d58b6e4d858df1f35be255b7030fb63c44
|
||||
size 123522
|
||||
oid sha256:b9cd21b6d9a9d0a9ef656bf895d6d135ba76160695355ce034fe985991d6955b
|
||||
size 123949
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:456dc57223044ece1f4136dc01765e41a0b649c827ad6e6b08370e53a7015dbb
|
||||
size 14378
|
||||
oid sha256:2dce0794e50116043fde9f6eb76b0c05ca21fc9cee26bde01618024c24c154b2
|
||||
size 14952
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5fdeb06fb15486d2d4949a38da2f6eed5ef94a0386e9daaa83a6b00bec9e392
|
||||
size 137096
|
||||
oid sha256:f3cea16c42488306fe29fd22362a0d37c1eeb0fbb1db48f26ad7ff18f6b196ae
|
||||
size 137605
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9b64a6f9fe548897b6b34961bc077718265acdb8f894d5d2d697d5409c633368
|
||||
size 137247
|
||||
oid sha256:58655e1846dbe73c287073ac909ee6c881a0ad800e8852653116fdbfd043b9a0
|
||||
size 137881
|
||||
|
||||
Reference in New Issue
Block a user