Media action: show snackbar when file saved on disk

This commit is contained in:
ganfra 2023-06-02 19:22:47 +02:00
parent f7f5539cb9
commit 31ba1a1a06
7 changed files with 60 additions and 35 deletions

View file

@ -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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions 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 io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -94,23 +95,11 @@ fun MessagesView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LogCompositions(tag = "MessagesScreen", msg = "Root") LogCompositions(tag = "MessagesScreen", msg = "Root")
val coroutineScope = rememberCoroutineScope()
var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) } var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments) AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val snackbarMessageText = state.snackbarMessage?.let { stringResource(it.messageResId) }
if (snackbarMessageText != null) {
SideEffect {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = snackbarMessageText,
duration = state.snackbarMessage.duration
)
}
}
}
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
val localView = LocalView.current val localView = LocalView.current

View file

@ -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.features.messages.impl.media.local.createFromMediaFile
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter 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.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import io.element.android.libraries.ui.strings.R as StringR
class MediaViewerPresenter @AssistedInject constructor( class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val inputs: MediaViewerNode.Inputs, @Assisted private val inputs: MediaViewerNode.Inputs,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val mediaActionsHandler: LocalMediaActions, private val mediaActionsHandler: LocalMediaActions,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<MediaViewerState> { ) : Presenter<MediaViewerState> {
@AssistedFactory @AssistedFactory
@ -60,6 +65,7 @@ class MediaViewerPresenter @AssistedInject constructor(
val localMedia: MutableState<Async<LocalMedia>> = remember { val localMedia: MutableState<Async<LocalMedia>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
DisposableEffect(loadMediaTrigger) { DisposableEffect(loadMediaTrigger) {
coroutineScope.downloadMedia(mediaFile, localMedia) coroutineScope.downloadMedia(mediaFile, localMedia)
onDispose { onDispose {
@ -81,6 +87,7 @@ class MediaViewerPresenter @AssistedInject constructor(
mimeType = inputs.mimeType, mimeType = inputs.mimeType,
thumbnailSource = inputs.thumbnailSource, thumbnailSource = inputs.thumbnailSource,
downloadedMedia = localMedia.value, downloadedMedia = localMedia.value,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
@ -105,7 +112,13 @@ class MediaViewerPresenter @AssistedInject constructor(
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch { private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
when (localMedia) { 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 else -> Unit
} }
} }

View file

@ -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.features.messages.impl.media.local.LocalMedia
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
data class MediaViewerState( data class MediaViewerState(
@ -25,5 +26,6 @@ data class MediaViewerState(
val mimeType: String?, val mimeType: String?,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val downloadedMedia: Async<LocalMedia>, val downloadedMedia: Async<LocalMedia>,
val snackbarMessage: SnackbarMessage?,
val eventSink: (MediaViewerEvents) -> Unit, val eventSink: (MediaViewerEvents) -> Unit,
) )

View file

@ -50,4 +50,5 @@ fun aMediaViewerState(downloadedMedia: Async<LocalMedia> = Async.Uninitialized)
mimeType = MimeTypes.IMAGE_JPEG, mimeType = MimeTypes.IMAGE_JPEG,
thumbnailSource = null, thumbnailSource = null,
downloadedMedia = downloadedMedia, downloadedMedia = downloadedMedia,
snackbarMessage = null
) {} ) {}

View file

@ -28,6 +28,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3Api 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar 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.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.ui.strings.R.* import io.element.android.libraries.ui.strings.R.*
@ -96,10 +100,21 @@ fun MediaViewerView(
showThumbnail = false showThumbnail = false
} }
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
Scaffold(modifier, Scaffold(modifier,
topBar = { topBar = {
MediaViewerTopBar(onBackPressed, state.eventSink) MediaViewerTopBar(onBackPressed, state.eventSink)
} },
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
snackbarData = data,
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.primary
)
}
},
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier

View file

@ -40,17 +40,13 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 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.components.TextButton
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
import io.element.android.libraries.designsystem.utils.LogCompositions 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 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.designsystem.R as DrawableR
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@ -179,21 +175,7 @@ fun RoomListContent(
} }
} }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
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,
)
}
}
}
Scaffold( Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),

View file

@ -18,10 +18,14 @@ package io.element.android.libraries.designsystem.utils
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -64,6 +68,25 @@ fun handleSnackbarMessage(
return snackbarMessage 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( data class SnackbarMessage(
@StringRes val messageResId: Int, @StringRes val messageResId: Int,
val duration: SnackbarDuration = SnackbarDuration.Short, val duration: SnackbarDuration = SnackbarDuration.Short,