Media action: show snackbar when file saved on disk
This commit is contained in:
parent
f7f5539cb9
commit
31ba1a1a06
7 changed files with 60 additions and 35 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
Loading…
Add table
Add a link
Reference in a new issue