Merge pull request #6643 from element-hq/feature/bma/updateMediaViewer
Update media viewer UI
This commit is contained in:
commit
1b2eb3a375
78 changed files with 722 additions and 456 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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")),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e40914567317a9cbecd3638bf23cba6ffbaa9be24733909aa58cb8d84a64c463
|
oid sha256:4c1298d7a7bef09a72be97f859448a877594088482de6e1fc2660e1a5dcb0869
|
||||||
size 31169
|
size 31354
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19e65122fd39fbf25050c9e982285fea34c4fec06647d713c8ea78c2c813ea5a
|
oid sha256:621db291024ceaa9bba2bc3e71a68b6fc67dab8055ede0595f0519499324b483
|
||||||
size 30478
|
size 30644
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e40914567317a9cbecd3638bf23cba6ffbaa9be24733909aa58cb8d84a64c463
|
oid sha256:4c1298d7a7bef09a72be97f859448a877594088482de6e1fc2660e1a5dcb0869
|
||||||
size 31169
|
size 31354
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19e65122fd39fbf25050c9e982285fea34c4fec06647d713c8ea78c2c813ea5a
|
oid sha256:621db291024ceaa9bba2bc3e71a68b6fc67dab8055ede0595f0519499324b483
|
||||||
size 30478
|
size 30644
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:08a9c400585956485c4a18bafa78ba4fa8eee8b98fce657077777686871041b5
|
oid sha256:2222b0d2d1589df67d73aad8eee9a1f30efb1392e0c5493f47f69cac01c8710b
|
||||||
size 31010
|
size 31044
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:20fa72623728a8f1b8206af75dcba3b83384aeab2977fde952bc4d3000aa7d7b
|
||||||
|
size 43579
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6e7825e195725479409a23b626771fe207debec55953c0869e5dd9bcae210dc7
|
oid sha256:ada067f89b469fccaaf4b9751c0469a9f776ef6206d5ebbb68b7b2128d77ce3c
|
||||||
size 29529
|
size 29564
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f717f0e4ea67364e505e8c756931d11bdb696d7be5f11bb155f085c9f2a668cc
|
||||||
|
size 42149
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:67be1e8e93d0c10366f4b37f14ca0f90c1f4ccd5da9024c9a396d3d46507e18c
|
||||||
|
size 40339
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3f89a1039bc2d101af319c0b6259d8eebc9d313ae34947aedee525945be23799
|
||||||
|
size 44604
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6144f221b9d11c70d15b54321bfbff3d1de1454e6c73be34d7b2e82bd1625a94
|
||||||
|
size 30701
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a512016e96b933f6089e60b634fa77f140dfa7de50d3a8c1a62092ad552d7f2d
|
||||||
|
size 39216
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8608e63ef9ff79fd698c202d60a8c73f8874f8610601a0ff6dc85663ed82c7da
|
||||||
|
size 43537
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2898f00a828a69ce9cdd56994d7878f6aae8c5b2ea5d1150df57aa2ebd7e537b
|
||||||
|
size 29233
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9e514d7f36fe25150d66c2d2092982a696196a5a0a2674eef97b7231c0c03bef
|
oid sha256:64d834437c7049ec9d81331b382c4bf0cdbc603de8e403e0addf6a4947e815a6
|
||||||
size 699410
|
size 700141
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:712af2723a0656ffe927f1cd488d3117f5b450fa405dad307b0c88a9f2483f9c
|
oid sha256:398c2908d73fb30117ce917b25e930c578eaf2da5e0c41e126f262f129bda8cc
|
||||||
size 698704
|
size 699710
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:010120cbcbfed1e7bfa3fb4c88df6e1098d0382cb1834f1a8046e312434f201d
|
oid sha256:89da32776c436ca00045afb76f42c5f0e5c59f6d682d59a3204461b10ea95474
|
||||||
size 26549
|
size 26617
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:99478fe3d9d44e9cda39f33748c70017203a2cce5cb855605740a29360221bd5
|
oid sha256:348a194ad17a0a0dffda47c7387edde99436d543e9bff1726608946e92558830
|
||||||
size 184910
|
size 185553
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3effa629295c12d2248924cf298599f3bd28975ecc76fac84f771c16266d738e
|
oid sha256:b23ef4fb29f51e308b74681017fcee257308edbd1e218e4daf5736501c70e0de
|
||||||
size 698895
|
size 699637
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1fd3b7065860d1e51fe51a0a6e3f4a9e61a77ea91cefe85cc3aa3560c4cc6bb2
|
oid sha256:af91214346ed087a8cf936b9ddc5b78e8840a48f6dab6b856ad79abb5fef1ea0
|
||||||
size 252253
|
size 252778
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9eb950f3c0d34a796ebd7635ff736023742d5e6a243912f3ad2234ecf08694b2
|
oid sha256:1fcaacd813136d9cb22d0542480fcef2be05cae7ec44c0c3683874086e1c7a4b
|
||||||
size 183220
|
size 184107
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2b5af8a5aae566e72e0223d40a5978ab191911c68739e417503d8fd5058f19e7
|
oid sha256:f17669c1a3a65bafa5786ac23245117b6956d154a4ddea2e95de9070a5bff07e
|
||||||
size 389408
|
size 390004
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:aec4a5e70320024eb053c56445c226758818550745f2d2ddfec16721e43b6d90
|
oid sha256:2bd5c7a42aebb6c2ebce9a529bafb23145e5a3a1d10270385386ecde9ca03c19
|
||||||
size 388674
|
size 389486
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1e53770850ee2e0d011ed91360a2c02abf80b27cc2cee66699331684637d469a
|
oid sha256:4c9c5240346788914d7ab6a825c38f6fc2d6dba8bdc2879c2657cdafe3718b34
|
||||||
size 30960
|
size 31511
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:df718693c63e4de07d17486a80103240f8f2d56a21aaf1a2b18a9237f105364d
|
oid sha256:730cffc57e88440ec4607238b383e68aada1907e6f9829a3bdc4bf8a467c1788
|
||||||
size 113630
|
size 114080
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d0ed6080a007c98867a5c4f9b32570ab3042b34030b5210867110374064ee8fc
|
oid sha256:9d8a4b42d3857ddaa1ea86d4ab3a872c5fa0014061b2a116895616bee25424d1
|
||||||
size 389436
|
size 390037
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4feb42b25bd952a0305ad2b1c4cab9934586693d804336e1ba23e9b247f712c0
|
oid sha256:1c4b715349adcfdf5c3cdd95844846170b43dc191801156b2e822f0eaeb92033
|
||||||
size 94961
|
size 95392
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:04ba5da6b618ae3abf4ca06b4ae1baa13e46673b0be73ada2b15cefd79063122
|
oid sha256:3aef5ebd6889b0fc8345db6aefeb6d2cc26a5a2349632dac2e258caa87c28b53
|
||||||
size 112146
|
size 112850
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue