From 31ba1a1a06b31e15e7afd800d22288a084bb75b6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 Jun 2023 19:22:47 +0200 Subject: [PATCH] Media action: show snackbar when file saved on disk --- .../features/messages/impl/MessagesView.kt | 15 ++---------- .../impl/media/viewer/MediaViewerPresenter.kt | 15 +++++++++++- .../impl/media/viewer/MediaViewerState.kt | 2 ++ .../media/viewer/MediaViewerStateProvider.kt | 1 + .../impl/media/viewer/MediaViewerView.kt | 17 +++++++++++++- .../features/roomlist/impl/RoomListView.kt | 22 ++---------------- .../{SnackbarDispatcher.kt => Snackbar.kt} | 23 +++++++++++++++++++ 7 files changed, 60 insertions(+), 35 deletions(-) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/{SnackbarDispatcher.kt => Snackbar.kt} (72%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 579dfeaf5a..05ee38a2ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -76,6 +76,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions +import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @@ -94,23 +95,11 @@ fun MessagesView( modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") - val coroutineScope = rememberCoroutineScope() var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) } AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments) - val snackbarHostState = remember { SnackbarHostState() } - val snackbarMessageText = state.snackbarMessage?.let { stringResource(it.messageResId) } - if (snackbarMessageText != null) { - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = snackbarMessageText, - duration = state.snackbarMessage.duration - ) - } - } - } + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose val localView = LocalView.current diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index b8e175424a..65c84f70b1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -33,16 +33,21 @@ import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.features.messages.impl.media.local.createFromMediaFile import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.handleSnackbarMessage import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import io.element.android.libraries.ui.strings.R as StringR class MediaViewerPresenter @AssistedInject constructor( @Assisted private val inputs: MediaViewerNode.Inputs, private val localMediaFactory: LocalMediaFactory, private val mediaLoader: MatrixMediaLoader, private val mediaActionsHandler: LocalMediaActions, + private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { @AssistedFactory @@ -60,6 +65,7 @@ class MediaViewerPresenter @AssistedInject constructor( val localMedia: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) DisposableEffect(loadMediaTrigger) { coroutineScope.downloadMedia(mediaFile, localMedia) onDispose { @@ -81,6 +87,7 @@ class MediaViewerPresenter @AssistedInject constructor( mimeType = inputs.mimeType, thumbnailSource = inputs.thumbnailSource, downloadedMedia = localMedia.value, + snackbarMessage = snackbarMessage, eventSink = ::handleEvents ) } @@ -105,7 +112,13 @@ class MediaViewerPresenter @AssistedInject constructor( private fun CoroutineScope.saveOnDisk(localMedia: Async) = launch { when (localMedia) { - is Async.Success -> mediaActionsHandler.saveOnDisk(localMedia.state) + is Async.Success -> { + mediaActionsHandler.saveOnDisk(localMedia.state) + .onSuccess { + val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android) + snackbarDispatcher.post(snackbarMessage) + } + } else -> Unit } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index c42263dd3e..77950a4bf3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.media.viewer import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.media.MediaSource data class MediaViewerState( @@ -25,5 +26,6 @@ data class MediaViewerState( val mimeType: String?, val thumbnailSource: MediaSource?, val downloadedMedia: Async, + val snackbarMessage: SnackbarMessage?, val eventSink: (MediaViewerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 6c54eb3b05..4f12be9ba9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -50,4 +50,5 @@ fun aMediaViewerState(downloadedMedia: Async = Async.Uninitialized) mimeType = MimeTypes.IMAGE_JPEG, thumbnailSource = null, downloadedMedia = downloadedMedia, + snackbarMessage = null ) {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 721cb76350..24efe4dbe9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -28,6 +28,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -53,6 +56,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.ui.strings.R.* @@ -96,10 +100,21 @@ fun MediaViewerView( showThumbnail = false } + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) + Scaffold(modifier, topBar = { MediaViewerTopBar(onBackPressed, state.eventSink) - } + }, + snackbarHost = { + SnackbarHost(snackbarHostState) { data -> + Snackbar( + snackbarData = data, + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.primary + ) + } + }, ) { Box( modifier = Modifier diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 49b2c3604c..112d1afb75 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -40,17 +40,13 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -80,8 +76,8 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator import io.element.android.libraries.designsystem.utils.LogCompositions +import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.coroutines.launch import io.element.android.libraries.designsystem.R as DrawableR import io.element.android.libraries.ui.strings.R as StringR @@ -179,21 +175,7 @@ fun RoomListContent( } } - val snackbarHostState = remember { SnackbarHostState() } - val snackbarMessageText = if (state.snackbarMessage != null) { - stringResource(state.snackbarMessage.messageResId) - } else null - val coroutineScope = rememberCoroutineScope() - if (snackbarMessageText != null) { - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = snackbarMessageText, - duration = SnackbarDuration.Short, - ) - } - } - } + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt similarity index 72% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt index 1131777398..9a31b92182 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcher.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt @@ -18,10 +18,14 @@ package io.element.android.libraries.designsystem.utils import androidx.annotation.StringRes import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.stringResource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -64,6 +68,25 @@ fun handleSnackbarMessage( return snackbarMessage } +@Composable +fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostState { + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val snackbarMessageText = snackbarMessage?.let { + stringResource(id = snackbarMessage.messageResId) + } + LaunchedEffect(snackbarMessage) { + if (snackbarMessageText == null) return@LaunchedEffect + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = snackbarMessageText, + duration = snackbarMessage.duration, + ) + } + } + return snackbarHostState +} + data class SnackbarMessage( @StringRes val messageResId: Int, val duration: SnackbarDuration = SnackbarDuration.Short,