Add discard changes warning dialog to media-send module.

This commit is contained in:
Alex Hart
2026-08-10 16:32:57 -04:00
committed by Greyson Parrelli
parent 39268ca45b
commit a4793ebba5
4 changed files with 55 additions and 6 deletions
@@ -228,10 +228,7 @@ class MediaSendV3Activity :
.show(supportFragmentManager)
}
is MediaSendFlowHudCommand.CloseScreen -> {
// TODO [media-send] warning dialog
finish()
}
is MediaSendFlowHudCommand.CloseScreen -> finish()
}
}
)
@@ -128,6 +128,7 @@ class MediaSendFlowViewModel(
internal val usernameScannedDialog = DialogController<String>()
internal val linkedDeviceScannedDialog = DialogController<Unit>()
internal val saveToStorageDialog = DialogController<Unit>()
internal val discardMediaDialog = DialogController<Unit>()
internal val addToGroupStoryDialog = DialogController<MediaRecipientId>()
internal val writeStoragePermission = PermissionController(
@@ -260,6 +261,23 @@ class MediaSendFlowViewModel(
}
}
/**
* Leaves the flow at the user's request, confirming first if that would throw a selection away. Closing for reasons
* of our own emits [MediaSendFlowHudCommand.CloseScreen] directly instead.
*/
internal fun onCloseRequested() {
if (state.value.selectedMedia.isEmpty()) {
sendHudCommand(MediaSendFlowHudCommand.CloseScreen)
return
}
viewModelScope.launch {
if (discardMediaDialog.show(Unit) == DialogResult.POSITIVE) {
sendHudCommand(MediaSendFlowHudCommand.CloseScreen)
}
}
}
/**
* Backs out of a select screen while nothing is selected, stepping over an editor that would have nothing to edit
* and no toolbar to leave by. Decided here rather than when the selection empties, so that re-selecting something
@@ -269,7 +287,7 @@ class MediaSendFlowViewModel(
val destination = backStack.dropLast(1).dropLastWhile { it == MediaSendRoute.Edit }
if (destination.isEmpty()) {
sendHudCommand(MediaSendFlowHudCommand.CloseScreen)
onCloseRequested()
return
}
@@ -291,7 +309,7 @@ class MediaSendFlowViewModel(
private fun onCameraXScreenEvent(event: CameraXScreenEvents) {
when (event) {
CameraXScreenEvents.CameraCloseClicked -> sendHudCommand(MediaSendFlowHudCommand.CloseScreen)
CameraXScreenEvents.CameraCloseClicked -> onCloseRequested()
CameraXScreenEvents.GalleryClicked -> backStack.goToFolders()
is CameraXScreenEvents.ImageCaptured -> handleImageCaptured(event)
is CameraXScreenEvents.VideoCaptured -> handleVideoCaptured(event)
@@ -1,6 +1,7 @@
package org.signal.mediasend
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -17,6 +18,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -29,6 +31,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.signal.core.ui.compose.DialogController
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.showSnackbar
@@ -129,6 +132,13 @@ internal fun MediaSendNavigation(
}
}
// NavDisplay only consumes back while there is somewhere left to go back to. Composed after it to catch the press
// at the root, which would otherwise fall through to the activity and finish it.
BackHandler(enabled = viewModel.backStack.size == 1) {
viewModel.onCloseRequested()
}
DiscardMediaDialog(viewModel.discardMediaDialog)
Snackbar(viewModel.snackbarEvents)
Toast(viewModel.toastEvents)
SendProgress(viewModel.state)
@@ -138,6 +148,26 @@ internal fun MediaSendNavigation(
private val TOAST_DURATION = 3.seconds
private val SEND_PROGRESS_DELAY = 300.milliseconds
/**
* Dialog displayed when the user tries to close out of media send, to warn them that they'll discard media.
*/
@Composable
private fun DiscardMediaDialog(
controller: DialogController<Unit>
) {
controller.Content { _, onDismissRequest, onConfirm, _, onDeny ->
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.MediaSendDialogs__discard_media),
body = stringResource(R.string.MediaSendDialogs__you_will_lose_any_media),
confirm = stringResource(R.string.MediaSendDialogs__discard),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onConfirm,
onDeny = onDeny,
onDismissRequest = onDismissRequest
)
}
}
/**
* Covers the whole flow while a send is in flight, so that the media on its way out cannot be edited or resent. Sends
* that resolve immediately never show it.
@@ -6,6 +6,10 @@
<string name="MediaSendDialogs__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
<!-- Confirmation button that discards the user\'s image edits. -->
<string name="MediaSendDialogs__discard">Discard</string>
<!-- Dialog title shown when the user tries to close media send while media is selected. -->
<string name="MediaSendDialogs__discard_media">Discard media?</string>
<!-- Dialog body explaining that leaving media send will discard the selected media along with any edits. -->
<string name="MediaSendDialogs__you_will_lose_any_media">You will lose any media you captured and changes you made.</string>
<!-- Dialog title asking to save the media being edited to the phone\'s storage -->
<string name="MediaSendDialogs__save_to_phone">Save to phone?</string>
<!-- Dialog message explaining that media will be saved to your phone and can potentially be accessed by other apps. -->