Move "Open with" action to bottom sheet
This commit is contained in:
parent
a0632b216c
commit
83b4bfad96
7 changed files with 70 additions and 41 deletions
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -57,6 +58,7 @@ fun MediaDetailsBottomSheet(
|
|||
onShare: (EventId) -> Unit,
|
||||
onForward: (EventId) -> Unit,
|
||||
onDownload: (EventId) -> Unit,
|
||||
onOpenWith: (EventId) -> Unit,
|
||||
onDelete: (EventId) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -128,6 +130,26 @@ fun MediaDetailsBottomSheet(
|
|||
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)
|
||||
}
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
leadingContent = icon,
|
||||
headlineContent = { Text(wording) },
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = {
|
||||
onOpenWith(state.eventId)
|
||||
}
|
||||
)
|
||||
if (state.canDelete) {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
|
|
@ -236,6 +258,7 @@ internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
|
|||
onShare = {},
|
||||
onForward = {},
|
||||
onDownload = {},
|
||||
onOpenWith = {},
|
||||
onDelete = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ sealed interface MediaGalleryEvents {
|
|||
data class Share(val eventId: EventId) : MediaGalleryEvents
|
||||
data class Forward(val eventId: EventId) : MediaGalleryEvents
|
||||
data class SaveOnDisk(val eventId: EventId) : MediaGalleryEvents
|
||||
data class OpenWith(val eventId: EventId) : MediaGalleryEvents
|
||||
data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
|
||||
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,12 @@ class MediaGalleryPresenter(
|
|||
saveOnDisk(it)
|
||||
}
|
||||
}
|
||||
is MediaGalleryEvents.OpenWith -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
||||
openWith(it)
|
||||
}
|
||||
}
|
||||
is MediaGalleryEvents.Share -> coroutineScope.launch {
|
||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
||||
|
|
@ -200,6 +206,17 @@ class MediaGalleryPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun openWith(mediaItem: MediaItem.Event) {
|
||||
downloadMedia(mediaItem)
|
||||
.mapCatchingExceptions { localMedia ->
|
||||
localMediaActions.open(localMedia)
|
||||
}
|
||||
.onFailure {
|
||||
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
||||
snackbarDispatcher.post(snackbarMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mediaActionsError(throwable: Throwable): Int {
|
||||
return if (throwable is ActivityNotFoundException) {
|
||||
R.string.error_no_compatible_app_found
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ fun MediaGalleryView(
|
|||
onDownload = { eventId ->
|
||||
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
|
||||
},
|
||||
onOpenWith = { eventId ->
|
||||
state.eventSink(MediaGalleryEvents.OpenWith(eventId))
|
||||
},
|
||||
onDelete = { eventId ->
|
||||
state.eventSink(
|
||||
MediaGalleryEvents.ConfirmDelete(
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.audio.api.AudioFocus
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
|
|
@ -85,7 +84,6 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
|||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.impl.R
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
|
||||
|
|
@ -226,7 +224,6 @@ fun MediaViewerView(
|
|||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
|
||||
},
|
||||
eventSink = state.eventSink
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -275,6 +272,11 @@ fun MediaViewerView(
|
|||
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
|
||||
}
|
||||
},
|
||||
onOpenWith = {
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(MediaViewerEvent.OpenWith(currentData))
|
||||
}
|
||||
},
|
||||
onDelete = { eventId ->
|
||||
(currentData as? MediaViewerPageData.MediaViewerData)?.let {
|
||||
state.eventSink(
|
||||
|
|
@ -469,11 +471,9 @@ private fun MediaViewerTopBar(
|
|||
onShareClick: () -> Unit,
|
||||
onSaveClick: () -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
eventSink: (MediaViewerEvent) -> Unit,
|
||||
) {
|
||||
val downloadedMedia by data.downloadedMedia
|
||||
val actionsEnabled = downloadedMedia.isSuccess()
|
||||
val mimeType = data.mediaInfo.mimeType
|
||||
val senderName = data.mediaInfo.senderName
|
||||
val dateSent = data.mediaInfo.dateSent
|
||||
TopAppBar(
|
||||
|
|
@ -508,23 +508,6 @@ private fun MediaViewerTopBar(
|
|||
),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
actions = {
|
||||
IconButton(
|
||||
enabled = actionsEnabled,
|
||||
onClick = {
|
||||
eventSink(MediaViewerEvent.OpenWith(data))
|
||||
},
|
||||
) {
|
||||
when (mimeType) {
|
||||
MimeTypes.Apk -> Icon(
|
||||
resourceId = R.drawable.ic_apk_install,
|
||||
contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
|
||||
)
|
||||
else -> Icon(
|
||||
imageVector = CompoundIcons.PopOut(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_open_with)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = onShareClick,
|
||||
enabled = actionsEnabled,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMedia
|
|||
onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onForward: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOpenWith: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onDismiss: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
|
|
@ -126,6 +127,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMedia
|
|||
onShare = onShare,
|
||||
onForward = onForward,
|
||||
onDownload = onDownload,
|
||||
onOpenWith = onOpenWith,
|
||||
onDelete = onDelete,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer
|
|||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
|
|
@ -63,19 +64,7 @@ class MediaViewerViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on open emit expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
testMenuAction(
|
||||
data,
|
||||
CommonStrings.action_open_with,
|
||||
MediaViewerEvent.OpenWith(data),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on info emit expected Event`() {
|
||||
fun `clicking on info emits expected Event`() {
|
||||
val data = aMediaViewerPageData(
|
||||
downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)),
|
||||
)
|
||||
|
|
@ -112,7 +101,7 @@ class MediaViewerViewTest {
|
|||
|
||||
private fun testMenuAction(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
contentDescriptionRes: Int,
|
||||
@StringRes contentDescriptionRes: Int,
|
||||
expectedEvent: MediaViewerEvent,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
|
|
@ -135,7 +124,7 @@ class MediaViewerViewTest {
|
|||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on download emit expected Event`() {
|
||||
fun `clicking on download emits expected Event`() {
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
|
|
@ -146,7 +135,7 @@ class MediaViewerViewTest {
|
|||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `clicking on share emit expected Event`() {
|
||||
fun `clicking on share emits expected Event`() {
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
|
|
@ -155,9 +144,20 @@ class MediaViewerViewTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on open in emits expected Event`() {
|
||||
val data = aMediaViewerPageData()
|
||||
testBottomSheetAction(
|
||||
data,
|
||||
CommonStrings.action_open_with,
|
||||
MediaViewerEvent.OpenWith(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testBottomSheetAction(
|
||||
data: MediaViewerPageData.MediaViewerData,
|
||||
contentDescriptionRes: Int,
|
||||
@StringRes textRes: Int,
|
||||
expectedEvent: MediaViewerEvent,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||
|
|
@ -168,7 +168,7 @@ class MediaViewerViewTest {
|
|||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(contentDescriptionRes)
|
||||
rule.clickOn(textRes)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
MediaViewerEvent.OnNavigateTo(0),
|
||||
|
|
@ -188,7 +188,7 @@ class MediaViewerViewTest {
|
|||
state = state,
|
||||
)
|
||||
// Ensure that the action are visible
|
||||
val contentDescription = rule.activity.getString(CommonStrings.action_open_with)
|
||||
val contentDescription = rule.activity.getString(CommonStrings.action_share)
|
||||
rule.onNodeWithContentDescription(contentDescription)
|
||||
.assertExists()
|
||||
.assertHasClickAction()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue