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