Move "Open with" action to bottom sheet
This commit is contained in:
parent
ab5b1217ce
commit
34574fc391
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 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
|
||||||
|
|
@ -57,6 +58,7 @@ fun MediaDetailsBottomSheet(
|
||||||
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,
|
||||||
|
|
@ -128,6 +130,26 @@ fun MediaDetailsBottomSheet(
|
||||||
onDownload(state.eventId)
|
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) {
|
if (state.canDelete) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
@ -236,6 +258,7 @@ internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
|
||||||
onShare = {},
|
onShare = {},
|
||||||
onForward = {},
|
onForward = {},
|
||||||
onDownload = {},
|
onDownload = {},
|
||||||
|
onOpenWith = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ sealed interface MediaGalleryEvents {
|
||||||
data class Share(val eventId: EventId) : MediaGalleryEvents
|
data class Share(val eventId: EventId) : MediaGalleryEvents
|
||||||
data class Forward(val eventId: EventId) : MediaGalleryEvents
|
data class Forward(val eventId: EventId) : MediaGalleryEvents
|
||||||
data class SaveOnDisk(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 OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
|
||||||
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents
|
data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,12 @@ class MediaGalleryPresenter(
|
||||||
saveOnDisk(it)
|
saveOnDisk(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is MediaGalleryEvents.OpenWith -> coroutineScope.launch {
|
||||||
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
|
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
||||||
|
openWith(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
is MediaGalleryEvents.Share -> coroutineScope.launch {
|
is MediaGalleryEvents.Share -> coroutineScope.launch {
|
||||||
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
mediaBottomSheetState = MediaBottomSheetState.Hidden
|
||||||
groupedMediaItems.dataOrNull().find(event.eventId)?.let {
|
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 {
|
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
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,9 @@ fun MediaGalleryView(
|
||||||
onDownload = { eventId ->
|
onDownload = { eventId ->
|
||||||
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
|
state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
|
||||||
},
|
},
|
||||||
|
onOpenWith = { eventId ->
|
||||||
|
state.eventSink(MediaGalleryEvents.OpenWith(eventId))
|
||||||
|
},
|
||||||
onDelete = { eventId ->
|
onDelete = { eventId ->
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
MediaGalleryEvents.ConfirmDelete(
|
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.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
|
||||||
|
|
@ -226,7 +224,6 @@ fun MediaViewerView(
|
||||||
onInfoClick = {
|
onInfoClick = {
|
||||||
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
|
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
|
||||||
},
|
},
|
||||||
eventSink = state.eventSink
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|
@ -275,6 +272,11 @@ fun MediaViewerView(
|
||||||
state.eventSink(MediaViewerEvent.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(
|
||||||
|
|
@ -469,11 +471,9 @@ private fun MediaViewerTopBar(
|
||||||
onShareClick: () -> Unit,
|
onShareClick: () -> Unit,
|
||||||
onSaveClick: () -> Unit,
|
onSaveClick: () -> Unit,
|
||||||
onInfoClick: () -> Unit,
|
onInfoClick: () -> Unit,
|
||||||
eventSink: (MediaViewerEvent) -> 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(
|
||||||
|
|
@ -508,23 +508,6 @@ private fun MediaViewerTopBar(
|
||||||
),
|
),
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||||
actions = {
|
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(
|
IconButton(
|
||||||
onClick = onShareClick,
|
onClick = onShareClick,
|
||||||
enabled = actionsEnabled,
|
enabled = actionsEnabled,
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMedia
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -63,19 +64,7 @@ class MediaViewerViewTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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,
|
|
||||||
MediaViewerEvent.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)),
|
||||||
)
|
)
|
||||||
|
|
@ -112,7 +101,7 @@ class MediaViewerViewTest {
|
||||||
|
|
||||||
private fun testMenuAction(
|
private fun testMenuAction(
|
||||||
data: MediaViewerPageData.MediaViewerData,
|
data: MediaViewerPageData.MediaViewerData,
|
||||||
contentDescriptionRes: Int,
|
@StringRes contentDescriptionRes: Int,
|
||||||
expectedEvent: MediaViewerEvent,
|
expectedEvent: MediaViewerEvent,
|
||||||
) {
|
) {
|
||||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||||
|
|
@ -135,7 +124,7 @@ class MediaViewerViewTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
fun `clicking on download emit expected Event`() {
|
fun `clicking on download emits expected Event`() {
|
||||||
val data = aMediaViewerPageData()
|
val data = aMediaViewerPageData()
|
||||||
testBottomSheetAction(
|
testBottomSheetAction(
|
||||||
data,
|
data,
|
||||||
|
|
@ -146,7 +135,7 @@ class MediaViewerViewTest {
|
||||||
|
|
||||||
@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,
|
||||||
|
|
@ -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(
|
private fun testBottomSheetAction(
|
||||||
data: MediaViewerPageData.MediaViewerData,
|
data: MediaViewerPageData.MediaViewerData,
|
||||||
contentDescriptionRes: Int,
|
@StringRes textRes: Int,
|
||||||
expectedEvent: MediaViewerEvent,
|
expectedEvent: MediaViewerEvent,
|
||||||
) {
|
) {
|
||||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||||
|
|
@ -168,7 +168,7 @@ class MediaViewerViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rule.clickOn(contentDescriptionRes)
|
rule.clickOn(textRes)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
MediaViewerEvent.OnNavigateTo(0),
|
MediaViewerEvent.OnNavigateTo(0),
|
||||||
|
|
@ -188,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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue