Normalize naming in media-send module with registration module.

This commit is contained in:
Alex Hart
2026-08-04 20:03:38 -03:00
parent a8c575e645
commit ebd6c56b6d
31 changed files with 428 additions and 410 deletions
@@ -33,10 +33,10 @@ import org.signal.core.util.getParcelableArrayListExtraCompat
import org.signal.core.util.getParcelableExtraCompat
import org.signal.core.util.logging.Log
import org.signal.core.util.overrideActivityTransitionCompat
import org.signal.mediasend.MediaSendNavKey
import org.signal.mediasend.MediaSendRoute
import org.signal.mediasend.MediaValidator
import org.signal.mediasend.screens.capture.MediaCaptureBottomBar
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvent
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvents
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
@@ -61,7 +61,7 @@ class MediaSelectionActivity :
EmojiEventListener,
EmojiSearchFragment.Callback {
private var selectedCaptureScreen: MediaSendNavKey.Capture by mutableStateOf(MediaSendNavKey.Capture.Camera)
private var selectedCaptureScreen: MediaSendRoute.Capture by mutableStateOf(MediaSendRoute.Capture.Camera)
private var isOnCaptureScreen: Boolean by mutableStateOf(false)
@@ -138,12 +138,12 @@ class MediaSelectionActivity :
selectedMedia = state?.selectedMedia ?: emptyList(),
onEvent = { event ->
when (event) {
MediaCaptureScreenEvent.ShowCamera -> debouncer.publish { popTextStoryPostCreationFragment() }
MediaCaptureScreenEvent.ShowTextStory -> viewModel.sendCommand(HudCommand.GoToText)
MediaCaptureScreenEvent.NextClicked -> viewModel.sendCommand(HudCommand.GoToReview)
is MediaCaptureScreenEvent.Camera -> Unit
MediaCaptureScreenEvent.CycleTextStoryBackgroundColor -> Unit
MediaCaptureScreenEvent.AddLinkToTextStory -> Unit
MediaCaptureScreenEvents.ShowCamera -> debouncer.publish { popTextStoryPostCreationFragment() }
MediaCaptureScreenEvents.ShowTextStory -> viewModel.sendCommand(HudCommand.GoToText)
MediaCaptureScreenEvents.NextClicked -> viewModel.sendCommand(HudCommand.GoToReview)
is MediaCaptureScreenEvents.Camera -> Unit
MediaCaptureScreenEvents.CycleTextStoryBackgroundColor -> Unit
MediaCaptureScreenEvents.AddLinkToTextStory -> Unit
}
},
modifier = Modifier.navigationBarsPadding()
@@ -168,7 +168,7 @@ class MediaSelectionActivity :
(supportFragmentManager.findFragmentByTag(NAV_HOST_TAG) as NavHostFragment).navController.addOnDestinationChangedListener { _, d, _ ->
when (d.id) {
R.id.mediaCaptureFragment -> {
selectedCaptureScreen = MediaSendNavKey.Capture.Camera
selectedCaptureScreen = MediaSendRoute.Capture.Camera
isOnCaptureScreen = true
requestedOrientation = if (resources.getWindowBreakpoint() is WindowBreakpoint.Small) {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
@@ -178,7 +178,7 @@ class MediaSelectionActivity :
}
R.id.textStoryPostCreationFragment -> {
selectedCaptureScreen = MediaSendNavKey.Capture.TextStory
selectedCaptureScreen = MediaSendRoute.Capture.TextStory
isOnCaptureScreen = true
requestedOrientation = if (resources.getWindowBreakpoint() is WindowBreakpoint.Small) {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
@@ -26,7 +26,7 @@ import org.signal.core.ui.view.Stub
import org.signal.core.util.ByteLimitInputFilter
import org.signal.core.util.EditTextUtil
import org.signal.core.util.getParcelableCompat
import org.signal.mediasend.HudCommand
import org.signal.mediasend.MediaSendFlowHudCommand
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.warning.guardAgainstRecoveryKeyPaste
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
@@ -339,7 +339,7 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
const val RESULT_INCREMENT_VIEW_ONCE_STATE = "AddMessageViewModel_IncrementViewOnceState"
const val RESULT_MESSAGE = "AddMessageViewModel__Message"
fun show(fragmentManager: FragmentManager, addAMessageDialog: HudCommand.ShowAddAMessageDialog, destination: RecipientId?) {
fun show(fragmentManager: FragmentManager, addAMessageDialog: MediaSendFlowHudCommand.ShowAddAMessageDialog, destination: RecipientId?) {
return show(fragmentManager, addAMessageDialog.message, addAMessageDialog.startWithEmojiKeyboard, addAMessageDialog.isViewOnceAvailable, destination)
}
@@ -33,7 +33,7 @@ import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.getWindowBreakpoint
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.dp
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvent
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvents
import org.signal.mediasend.screens.capture.TextStoryHorizontalBar
import org.signal.mediasend.screens.capture.TextStoryVerticalBar
import org.thoughtcrime.securesms.R
@@ -256,10 +256,10 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati
binding.scene.addView(composeView)
}
private fun onTextStoryBarEvent(event: MediaCaptureScreenEvent) {
private fun onTextStoryBarEvent(event: MediaCaptureScreenEvents) {
when (event) {
MediaCaptureScreenEvent.CycleTextStoryBackgroundColor -> viewModel.cycleBackgroundColor()
MediaCaptureScreenEvent.AddLinkToTextStory -> TextStoryPostLinkEntryFragment().show(childFragmentManager, null)
MediaCaptureScreenEvents.CycleTextStoryBackgroundColor -> viewModel.cycleBackgroundColor()
MediaCaptureScreenEvents.AddLinkToTextStory -> TextStoryPostLinkEntryFragment().show(childFragmentManager, null)
else -> Unit
}
}
@@ -31,11 +31,11 @@ import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.compose.LocalChatColorProvider
import org.signal.core.ui.compose.LocalDisplayNameProvider
import org.signal.core.ui.getWindowBreakpoint
import org.signal.mediasend.HudCommand
import org.signal.mediasend.MediaSendFlowActivityContract
import org.signal.mediasend.MediaSendFlowHudCommand
import org.signal.mediasend.MediaSendFlowViewModel
import org.signal.mediasend.MediaSendNavKey
import org.signal.mediasend.MediaSendRecipient
import org.signal.mediasend.MediaSendRoute
import org.signal.mediasend.MediaSendScreen
import org.signal.mediasend.screens.edit.LocalAddAMessageRowTextField
import org.signal.mediasend.screens.edit.LocalScheduledSendTimeFormatter
@@ -120,7 +120,7 @@ class MediaSendV3Activity :
setContent {
val context = LocalContext.current
val isOnCaptureScreen = viewModel.backStack.lastOrNull() is MediaSendNavKey.Capture
val isOnCaptureScreen = viewModel.backStack.lastOrNull() is MediaSendRoute.Capture
LaunchedEffect(isOnCaptureScreen) {
onCaptureScreenChanged(isOnCaptureScreen)
@@ -170,7 +170,7 @@ class MediaSendV3Activity :
},
onExternalHudCommand = {
when (it) {
is HudCommand.ShowAddAMessageDialog -> {
is MediaSendFlowHudCommand.ShowAddAMessageDialog -> {
AddMessageDialogFragment.show(
fragmentManager = supportFragmentManager,
addAMessageDialog = it,
@@ -180,19 +180,19 @@ class MediaSendV3Activity :
)
}
is HudCommand.SelectSticker -> stickerLauncher.launch(Unit)
is MediaSendFlowHudCommand.SelectSticker -> stickerLauncher.launch(Unit)
is HudCommand.PickScheduledSendTime -> {
is MediaSendFlowHudCommand.PickScheduledSendTime -> {
ScheduleMessageTimePickerBottomSheet.showSchedule(supportFragmentManager)
}
is HudCommand.ConfirmScheduledSend -> {
is MediaSendFlowHudCommand.ConfirmScheduledSend -> {
if (!ReenableScheduledMessagesDialogFragment.showIfNeeded(this, supportFragmentManager, null, it.scheduledTime)) {
viewModel.onScheduledSendConfirmed(it.scheduledTime)
}
}
is HudCommand.GoToConversation -> {
is MediaSendFlowHudCommand.GoToConversation -> {
lifecycleScope.launch(Dispatchers.Default) {
val recipient = Recipient.resolved(RecipientId.from(it.recipientId.id))
withContext(Dispatchers.Main) {
@@ -205,27 +205,27 @@ class MediaSendV3Activity :
}
}
HudCommand.GoToLinkedDevices -> {
MediaSendFlowHudCommand.GoToLinkedDevices -> {
startActivity(AppSettingsActivity.linkedDevices(this))
finish()
}
is HudCommand.GoToQuickTransfer -> {
is MediaSendFlowHudCommand.GoToQuickTransfer -> {
startActivity(QuickTransferOldDeviceActivity.intent(this, it.qrData))
finish()
}
is HudCommand.FinishWithResult -> finishWithResult(it.payload)
is MediaSendFlowHudCommand.FinishWithResult -> finishWithResult(it.payload)
is HudCommand.FinishWithoutResult -> onSentWithoutResult()
is MediaSendFlowHudCommand.FinishWithoutResult -> onSentWithoutResult()
is HudCommand.ResolveUntrustedIdentities -> {
is MediaSendFlowHudCommand.ResolveUntrustedIdentities -> {
SafetyNumberBottomSheet
.forRecipientIdsAndDestinations(it.untrustedRecipientIds.map(RecipientId::from), destinations())
.show(supportFragmentManager)
}
is HudCommand.CloseScreen -> {
is MediaSendFlowHudCommand.CloseScreen -> {
// TODO [media-send] warning dialog
finish()
}
@@ -0,0 +1,18 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.mediasend
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvents
import org.signal.mediasend.screens.edit.MediaEditScreenEvents
/**
* The screen events that the flow, rather than the screen that raised them, is responsible for.
* Implemented by [MediaSendFlowViewModel].
*/
interface MediaSendEventHandler {
fun onMediaEditScreenEvent(mediaEditScreenEvent: MediaEditScreenEvents)
fun onMediaCaptureScreenEvent(mediaCaptureScreenEvent: MediaCaptureScreenEvents)
}
@@ -5,83 +5,24 @@
package org.signal.mediasend
import android.os.Parcelable
import org.signal.core.models.media.Media
import org.signal.core.models.media.MediaFolder
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvent
import org.signal.mediasend.screens.edit.MediaEditScreenEvent
interface MediaSendEventHandler {
fun onMediaEditScreenEvent(mediaEditScreenEvent: MediaEditScreenEvent)
fun onMediaCaptureScreenEvent(mediaCaptureScreenEvent: MediaCaptureScreenEvent)
}
/**
* Changes to the flow itself, raised by the screens within it. A screen owns what only it renders; the selection, the
* back stack and the snackbar belong to the flow, so a screen asks for those through [MediaSendFlowViewModel.onEvent].
*/
internal sealed interface MediaSendEvent {
data class AddMedia(val media: Set<Media>) : MediaSendEvent
data class RemoveMedia(val media: Set<Media>) : MediaSendEvent
data class SetFocusedMedia(val media: Media) : MediaSendEvent
data class ReorderSelectedMedia(val fromIndex: Int, val toIndex: Int) : MediaSendEvent
data class ShowSnackbar(val snackbar: SnackbarEvent) : MediaSendEvent
internal sealed interface MediaSendFlowEvent {
data class AddMedia(val media: Set<Media>) : MediaSendFlowEvent
data class RemoveMedia(val media: Set<Media>) : MediaSendFlowEvent
data class SetFocusedMedia(val media: Media) : MediaSendFlowEvent
data class ReorderSelectedMedia(val fromIndex: Int, val toIndex: Int) : MediaSendFlowEvent
data class ShowSnackbar(val snackbar: SnackbarEvent) : MediaSendFlowEvent
/** Whoever was mid-gesture has stopped, so [MediaSendFlowState.isSelectionRejected] has served its purpose. */
data object SelectionRejectionShown : MediaSendEvent
data object SelectionRejectionShown : MediaSendFlowEvent
data class NavigateToFiles(val mediaFolder: MediaFolder) : MediaSendEvent
data object NavigateToEdit : MediaSendEvent
data object NavigateToCamera : MediaSendEvent
}
/**
* Commands sent from the ViewModel to the UI layer (HUD).
*
* These are one-shot events that don't belong in persistent state.
*/
sealed interface HudCommand {
/** Show the dialog to allow the user to add a message */
data class ShowAddAMessageDialog(
val message: CharSequence,
val startWithEmojiKeyboard: Boolean,
val isViewOnceAvailable: Boolean
) : HudCommand
/** Show the app's sticker picker. The pick is handed back via [MediaSendFlowViewModel.onStickerSelected]. */
data object SelectSticker : HudCommand
/**
* Show the app's date and time picker for a scheduled send. The pick is handed back via
* [MediaSendFlowViewModel.onScheduledSendTimeSelected].
*/
data object PickScheduledSendTime : HudCommand
/**
* Clear whatever the app requires before a send can be scheduled for [scheduledTime], such as the scheduled messages
* intro sheet or the exact alarm permission. Once cleared, the app calls
* [MediaSendFlowViewModel.onScheduledSendConfirmed] to let the send proceed.
*/
data class ConfirmScheduledSend(val scheduledTime: Long) : HudCommand
data class GoToConversation(val recipientId: MediaRecipientId) : HudCommand
data object GoToLinkedDevices : HudCommand
data class GoToQuickTransfer(val qrData: String) : HudCommand
data object CloseScreen : HudCommand
/**
* The send was handed off to the caller. [payload] is opaque to this module and is expected to be
* returned to whoever launched the flow.
*/
data class FinishWithResult(val payload: Parcelable) : HudCommand
/**
* The flow performed the send itself, so there is no payload. Callers still need to be told the send
* succeeded, as opposed to [CloseScreen] which is a cancellation.
*/
data object FinishWithoutResult : HudCommand
/** The send was blocked by safety number changes for [untrustedRecipientIds]. */
data class ResolveUntrustedIdentities(val untrustedRecipientIds: List<Long>) : HudCommand
data class NavigateToFiles(val mediaFolder: MediaFolder) : MediaSendFlowEvent
data object NavigateToEdit : MediaSendFlowEvent
data object NavigateToCamera : MediaSendFlowEvent
}
@@ -0,0 +1,59 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.mediasend
import android.os.Parcelable
/**
* Commands sent from the ViewModel to the UI layer (HUD).
*
* These are one-shot events that don't belong in persistent state.
*/
sealed interface MediaSendFlowHudCommand {
/** Show the dialog to allow the user to add a message */
data class ShowAddAMessageDialog(
val message: CharSequence,
val startWithEmojiKeyboard: Boolean,
val isViewOnceAvailable: Boolean
) : MediaSendFlowHudCommand
/** Show the app's sticker picker. The pick is handed back via [MediaSendFlowViewModel.onStickerSelected]. */
data object SelectSticker : MediaSendFlowHudCommand
/**
* Show the app's date and time picker for a scheduled send. The pick is handed back via
* [MediaSendFlowViewModel.onScheduledSendTimeSelected].
*/
data object PickScheduledSendTime : MediaSendFlowHudCommand
/**
* Clear whatever the app requires before a send can be scheduled for [scheduledTime], such as the scheduled messages
* intro sheet or the exact alarm permission. Once cleared, the app calls
* [MediaSendFlowViewModel.onScheduledSendConfirmed] to let the send proceed.
*/
data class ConfirmScheduledSend(val scheduledTime: Long) : MediaSendFlowHudCommand
data class GoToConversation(val recipientId: MediaRecipientId) : MediaSendFlowHudCommand
data object GoToLinkedDevices : MediaSendFlowHudCommand
data class GoToQuickTransfer(val qrData: String) : MediaSendFlowHudCommand
data object CloseScreen : MediaSendFlowHudCommand
/**
* The send was handed off to the caller. [payload] is opaque to this module and is expected to be
* returned to whoever launched the flow.
*/
data class FinishWithResult(val payload: Parcelable) : MediaSendFlowHudCommand
/**
* The flow performed the send itself, so there is no payload. Callers still need to be told the send
* succeeded, as opposed to [CloseScreen] which is a cancellation.
*/
data object FinishWithoutResult : MediaSendFlowHudCommand
/** The send was blocked by safety number changes for [untrustedRecipientIds]. */
data class ResolveUntrustedIdentities(val untrustedRecipientIds: List<Long>) : MediaSendFlowHudCommand
}
@@ -54,10 +54,10 @@ import org.signal.imageeditor.core.model.EditorModel
import org.signal.imageeditor.core.renderers.UriGlideRenderer
import org.signal.mediasend.preupload.PreUploadController
import org.signal.mediasend.preupload.PreUploadResult
import org.signal.mediasend.screens.capture.CameraXScreenEvent
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvent
import org.signal.mediasend.screens.capture.CameraXScreenEvents
import org.signal.mediasend.screens.capture.MediaCaptureScreenEvents
import org.signal.mediasend.screens.edit.ImageController
import org.signal.mediasend.screens.edit.MediaEditScreenEvent
import org.signal.mediasend.screens.edit.MediaEditScreenEvents
import org.signal.mediasend.screens.edit.ScheduleSendOption
import org.signal.mediasend.screens.edit.image.BrushTool
import org.signal.mediasend.screens.edit.image.BrushWidthsState
@@ -110,10 +110,10 @@ class MediaSendFlowViewModel(
key = KEY_BACK_STACK
) {
val startKey = when {
args.asTextStory -> MediaSendNavKey.Capture.TextStory
args.isCameraFirst -> MediaSendNavKey.Capture.Camera
args.initialMedia.isNotEmpty() -> MediaSendNavKey.Edit
else -> MediaSendNavKey.Select.Folders
args.asTextStory -> MediaSendRoute.Capture.TextStory
args.isCameraFirst -> MediaSendRoute.Capture.Camera
args.initialMedia.isNotEmpty() -> MediaSendRoute.Edit
else -> MediaSendRoute.Select.Folders
}
NavBackStack(startKey)
@@ -149,8 +149,8 @@ class MediaSendFlowViewModel(
}
/** One-shot HUD commands exposed as a Flow. */
private val hudCommandChannel = Channel<HudCommand>(Channel.BUFFERED)
val hudCommands: Flow<HudCommand> = hudCommandChannel.receiveAsFlow()
private val hudCommandChannel = Channel<MediaSendFlowHudCommand>(Channel.BUFFERED)
val hudCommands: Flow<MediaSendFlowHudCommand> = hudCommandChannel.receiveAsFlow()
/** Per-image editor controllers, held here so results arriving from outside the flow can be applied immediately. */
internal val imageControllers = ImageController.Container(BrushWidthsState(internalState.value.brushWidths))
@@ -190,15 +190,15 @@ class MediaSendFlowViewModel(
when (result) {
MediaSendQrRepository.QrCheckResult.LinkDevice -> {
when (linkedDeviceScannedDialog.show(Unit)) {
DialogResult.POSITIVE -> sendHudCommand(HudCommand.GoToLinkedDevices)
DialogResult.POSITIVE -> sendHudCommand(MediaSendFlowHudCommand.GoToLinkedDevices)
else -> Unit
}
}
MediaSendQrRepository.QrCheckResult.None -> Unit
is MediaSendQrRepository.QrCheckResult.ReRegistration -> sendHudCommand(HudCommand.GoToQuickTransfer(qrData))
is MediaSendQrRepository.QrCheckResult.ReRegistration -> sendHudCommand(MediaSendFlowHudCommand.GoToQuickTransfer(qrData))
is MediaSendQrRepository.QrCheckResult.Username -> {
when (usernameScannedDialog.show(result.username)) {
DialogResult.POSITIVE -> sendHudCommand(HudCommand.GoToConversation(result.recipientId))
DialogResult.POSITIVE -> sendHudCommand(MediaSendFlowHudCommand.GoToConversation(result.recipientId))
else -> Unit
}
}
@@ -244,65 +244,65 @@ class MediaSendFlowViewModel(
//region Media Selection
/** Applies a change a screen has asked for to the flow's own state. See [MediaSendEvent]. */
internal fun onEvent(event: MediaSendEvent) {
/** Applies a change a screen has asked for to the flow's own state. See [MediaSendFlowEvent]. */
internal fun onEvent(event: MediaSendFlowEvent) {
when (event) {
is MediaSendEvent.AddMedia -> addMedia(event.media)
is MediaSendEvent.RemoveMedia -> removeMedia(event.media)
is MediaSendEvent.SetFocusedMedia -> setFocusedMedia(event.media)
is MediaSendEvent.ReorderSelectedMedia -> reorderMedia(event.fromIndex, event.toIndex)
is MediaSendEvent.ShowSnackbar -> internalSnackbarEvents.trySend(event.snackbar)
MediaSendEvent.SelectionRejectionShown -> updateState { copy(isSelectionRejected = false) }
is MediaSendEvent.NavigateToFiles -> backStack.goToFiles(event.mediaFolder)
MediaSendEvent.NavigateToEdit -> backStack.goToEdit()
MediaSendEvent.NavigateToCamera -> backStack.goToCamera()
is MediaSendFlowEvent.AddMedia -> addMedia(event.media)
is MediaSendFlowEvent.RemoveMedia -> removeMedia(event.media)
is MediaSendFlowEvent.SetFocusedMedia -> setFocusedMedia(event.media)
is MediaSendFlowEvent.ReorderSelectedMedia -> reorderMedia(event.fromIndex, event.toIndex)
is MediaSendFlowEvent.ShowSnackbar -> internalSnackbarEvents.trySend(event.snackbar)
MediaSendFlowEvent.SelectionRejectionShown -> updateState { copy(isSelectionRejected = false) }
is MediaSendFlowEvent.NavigateToFiles -> backStack.goToFiles(event.mediaFolder)
MediaSendFlowEvent.NavigateToEdit -> backStack.goToEdit()
MediaSendFlowEvent.NavigateToCamera -> backStack.goToCamera()
}
}
override fun onMediaCaptureScreenEvent(mediaCaptureScreenEvent: MediaCaptureScreenEvent) {
override fun onMediaCaptureScreenEvent(mediaCaptureScreenEvent: MediaCaptureScreenEvents) {
when (mediaCaptureScreenEvent) {
MediaCaptureScreenEvent.ShowCamera -> backStack.goToCamera()
MediaCaptureScreenEvent.ShowTextStory -> backStack.goToTextStory()
is MediaCaptureScreenEvent.Camera -> onCameraXScreenEvent(mediaCaptureScreenEvent.event)
MediaCaptureScreenEvent.NextClicked -> backStack.goToEdit()
MediaCaptureScreenEvent.CycleTextStoryBackgroundColor -> error("Handled directly in the fragment.")
MediaCaptureScreenEvent.AddLinkToTextStory -> error("Handled directly in the fragment.")
MediaCaptureScreenEvents.ShowCamera -> backStack.goToCamera()
MediaCaptureScreenEvents.ShowTextStory -> backStack.goToTextStory()
is MediaCaptureScreenEvents.Camera -> onCameraXScreenEvent(mediaCaptureScreenEvent.event)
MediaCaptureScreenEvents.NextClicked -> backStack.goToEdit()
MediaCaptureScreenEvents.CycleTextStoryBackgroundColor -> error("Handled directly in the fragment.")
MediaCaptureScreenEvents.AddLinkToTextStory -> error("Handled directly in the fragment.")
}
}
private fun onCameraXScreenEvent(event: CameraXScreenEvent) {
private fun onCameraXScreenEvent(event: CameraXScreenEvents) {
when (event) {
CameraXScreenEvent.CameraCloseClicked -> sendHudCommand(HudCommand.CloseScreen)
CameraXScreenEvent.GalleryClicked -> backStack.goToFolders()
is CameraXScreenEvent.ImageCaptured -> handleImageCaptured(event)
is CameraXScreenEvent.VideoCaptured -> handleVideoCaptured(event)
is CameraXScreenEvent.QrCodeFound -> qrCheckRequest.trySend(event.data)
CameraXScreenEvent.VideoCaptureError -> {
CameraXScreenEvents.CameraCloseClicked -> sendHudCommand(MediaSendFlowHudCommand.CloseScreen)
CameraXScreenEvents.GalleryClicked -> backStack.goToFolders()
is CameraXScreenEvents.ImageCaptured -> handleImageCaptured(event)
is CameraXScreenEvents.VideoCaptured -> handleVideoCaptured(event)
is CameraXScreenEvents.QrCodeFound -> qrCheckRequest.trySend(event.data)
CameraXScreenEvents.VideoCaptureError -> {
internalSnackbarEvents.trySend(SnackbarEvent(message = R.string.MediaSendViewModel__error_recording_video))
}
}
}
override fun onMediaEditScreenEvent(mediaEditScreenEvent: MediaEditScreenEvent) {
override fun onMediaEditScreenEvent(mediaEditScreenEvent: MediaEditScreenEvents) {
when (mediaEditScreenEvent) {
is MediaEditScreenEvent.FocusedMediaChanged -> setFocusedMedia(mediaEditScreenEvent.media)
is MediaEditScreenEvent.ReorderSelectedMedia -> reorderMedia(mediaEditScreenEvent.fromIndex, mediaEditScreenEvent.toIndex)
MediaEditScreenEvent.NextClick -> onNextClick()
is MediaEditScreenEvent.ScheduleSendClick -> onScheduleSendClick(mediaEditScreenEvent.option)
MediaEditScreenEvent.NavigateBack -> onPopFromEdit()
is MediaEditScreenEvent.VideoTrimChanged -> onEditVideoDuration(
is MediaEditScreenEvents.FocusedMediaChanged -> setFocusedMedia(mediaEditScreenEvent.media)
is MediaEditScreenEvents.ReorderSelectedMedia -> reorderMedia(mediaEditScreenEvent.fromIndex, mediaEditScreenEvent.toIndex)
MediaEditScreenEvents.NextClick -> onNextClick()
is MediaEditScreenEvents.ScheduleSendClick -> onScheduleSendClick(mediaEditScreenEvent.option)
MediaEditScreenEvents.NavigateBack -> onPopFromEdit()
is MediaEditScreenEvents.VideoTrimChanged -> onEditVideoDuration(
totalDurationUs = mediaEditScreenEvent.videoTrimData.totalInputDurationUs,
startTimeUs = mediaEditScreenEvent.videoTrimData.startTimeUs,
endTimeUs = mediaEditScreenEvent.videoTrimData.endTimeUs,
touchEnabled = mediaEditScreenEvent.editingComplete
)
is MediaEditScreenEvent.VideoSeek -> error("VideoSeek is routed to the video player bus by MediaEditScreen and must not reach the view-model.")
is MediaEditScreenEvent.AddMessageClick -> {
is MediaEditScreenEvents.VideoSeek -> error("VideoSeek is routed to the video player bus by MediaEditScreen and must not reach the view-model.")
is MediaEditScreenEvents.AddMessageClick -> {
val snapshot: MediaSendFlowState = state.value
sendHudCommand(
HudCommand.ShowAddAMessageDialog(
MediaSendFlowHudCommand.ShowAddAMessageDialog(
message = snapshot.message ?: "",
startWithEmojiKeyboard = mediaEditScreenEvent.startWithEmojiKeyboard,
isViewOnceAvailable = snapshot.isViewOnceAvailable
@@ -310,42 +310,42 @@ class MediaSendFlowViewModel(
)
}
MediaEditScreenEvent.StickerClick -> {
sendHudCommand(HudCommand.SelectSticker)
MediaEditScreenEvents.StickerClick -> {
sendHudCommand(MediaSendFlowHudCommand.SelectSticker)
}
MediaEditScreenEvent.NavigateToGallery -> {
MediaEditScreenEvents.NavigateToGallery -> {
backStack.goToFolders()
}
is MediaEditScreenEvent.SetMediaQuality -> {
is MediaEditScreenEvents.SetMediaQuality -> {
setSentMediaQuality(mediaEditScreenEvent.quality)
}
MediaEditScreenEvent.ToggleViewOnce -> {
MediaEditScreenEvents.ToggleViewOnce -> {
toggleViewOnce()
}
MediaEditScreenEvent.SaveMedia -> {
MediaEditScreenEvents.SaveMedia -> {
saveFocusedMediaToStorage()
}
is MediaEditScreenEvent.RemoveMedia -> {
is MediaEditScreenEvents.RemoveMedia -> {
removeMedia(mediaEditScreenEvent.media)
}
is MediaEditScreenEvent.BrushWidthChanged -> {
is MediaEditScreenEvents.BrushWidthChanged -> {
setBrushWidth(mediaEditScreenEvent.tool, mediaEditScreenEvent.fraction)
}
is MediaEditScreenEvent.ToggleBlurFaces -> {
is MediaEditScreenEvents.ToggleBlurFaces -> {
setBlurFacesEnabled(mediaEditScreenEvent.enabled)
}
}
}
/**
* Result of the picker opened for [HudCommand.SelectSticker], applied to the focused image. A null [renderer] means
* Result of the picker opened for [MediaSendFlowHudCommand.SelectSticker], applied to the focused image. A null [renderer] means
* the picker was dismissed.
*/
fun onStickerSelected(renderer: Renderer?) {
@@ -389,7 +389,7 @@ class MediaSendFlowViewModel(
repository.brushWidths = brushWidths
}
private fun handleImageCaptured(imageCaptured: CameraXScreenEvent.ImageCaptured) {
private fun handleImageCaptured(imageCaptured: CameraXScreenEvents.ImageCaptured) {
viewModelScope.launch {
val media: Media? = withContext(Dispatchers.IO) {
try {
@@ -413,7 +413,7 @@ class MediaSendFlowViewModel(
}
}
private fun handleVideoCaptured(videoCaptured: CameraXScreenEvent.VideoCaptured) {
private fun handleVideoCaptured(videoCaptured: CameraXScreenEvents.VideoCaptured) {
viewModelScope.launch {
val media: Media? = withContext(Dispatchers.IO) {
try {
@@ -470,7 +470,7 @@ class MediaSendFlowViewModel(
backStack.goToEdit()
}
private fun sendHudCommand(hudCommand: HudCommand) {
private fun sendHudCommand(hudCommand: MediaSendFlowHudCommand) {
viewModelScope.launch {
hudCommandChannel.send(hudCommand)
}
@@ -578,8 +578,8 @@ class MediaSendFlowViewModel(
internalSnackbarEvents.trySend(SnackbarEvent(message = message))
updateState { copy(isSelectionRejected = true) }
if (isSelectionEmpty && backStack.lastOrNull() == MediaSendNavKey.Edit) {
backStack.resetTo(if (state.value.isCameraFirst) MediaSendNavKey.Capture.Camera else MediaSendNavKey.Select.Folders)
if (isSelectionEmpty && backStack.lastOrNull() == MediaSendRoute.Edit) {
backStack.resetTo(if (state.value.isCameraFirst) MediaSendRoute.Capture.Camera else MediaSendRoute.Select.Folders)
}
}
@@ -1127,17 +1127,17 @@ class MediaSendFlowViewModel(
private fun onScheduleSendClick(option: ScheduleSendOption) {
when (option) {
is ScheduleSendOption.PresetTime -> sendHudCommand(HudCommand.ConfirmScheduledSend(option.timeMs))
ScheduleSendOption.PickTime -> sendHudCommand(HudCommand.PickScheduledSendTime)
is ScheduleSendOption.PresetTime -> sendHudCommand(MediaSendFlowHudCommand.ConfirmScheduledSend(option.timeMs))
ScheduleSendOption.PickTime -> sendHudCommand(MediaSendFlowHudCommand.PickScheduledSendTime)
}
}
/**
* A time chosen in the picker opened for [HudCommand.PickScheduledSendTime]. It still has to clear the app's
* A time chosen in the picker opened for [MediaSendFlowHudCommand.PickScheduledSendTime]. It still has to clear the app's
* scheduling prerequisites, just like a time picked straight from the menu.
*/
fun onScheduledSendTimeSelected(scheduledTime: Long) {
sendHudCommand(HudCommand.ConfirmScheduledSend(scheduledTime))
sendHudCommand(MediaSendFlowHudCommand.ConfirmScheduledSend(scheduledTime))
}
/**
@@ -1223,18 +1223,18 @@ class MediaSendFlowViewModel(
viewModelScope.launch {
when (val result = send()) {
is SendResult.ReadyToSend -> sendHudCommand(HudCommand.FinishWithResult(result.payload))
is SendResult.Success -> sendHudCommand(HudCommand.FinishWithoutResult)
is SendResult.ReadyToSend -> sendHudCommand(MediaSendFlowHudCommand.FinishWithResult(result.payload))
is SendResult.Success -> sendHudCommand(MediaSendFlowHudCommand.FinishWithoutResult)
is SendResult.UntrustedIdentity -> {
updateState { copy(isSending = false) }
sendHudCommand(HudCommand.ResolveUntrustedIdentities(result.recipientIds))
sendHudCommand(MediaSendFlowHudCommand.ResolveUntrustedIdentities(result.recipientIds))
}
is SendResult.Error -> {
Log.w(TAG, "Send failed: ${result.message}")
updateState { copy(isSending = false) }
sendHudCommand(HudCommand.CloseScreen)
sendHudCommand(MediaSendFlowHudCommand.CloseScreen)
}
}
}
@@ -58,7 +58,7 @@ internal fun MediaSendNavigation(
)
) { key ->
when (key) {
is MediaSendNavKey.Capture -> NavEntry(MediaSendNavKey.Capture.Chrome) {
is MediaSendRoute.Capture -> NavEntry(MediaSendRoute.Capture.Chrome) {
val state by viewModel.state.collectAsStateWithLifecycle()
MediaCaptureScreen(
@@ -69,7 +69,7 @@ internal fun MediaSendNavigation(
)
}
MediaSendNavKey.Select.Folders -> NavEntry(key) {
MediaSendRoute.Select.Folders -> NavEntry(key) {
val selectViewModel: MediaSelectViewModel = viewModel(
factory = MediaSelectViewModel.Factory(
parentState = viewModel.state,
@@ -87,7 +87,7 @@ internal fun MediaSendNavigation(
)
}
is MediaSendNavKey.Select.Files -> NavEntry(key) {
is MediaSendRoute.Select.Files -> NavEntry(key) {
val selectViewModel: MediaSelectViewModel = viewModel(
factory = MediaSelectViewModel.Factory(
parentState = viewModel.state,
@@ -105,7 +105,7 @@ internal fun MediaSendNavigation(
)
}
is MediaSendNavKey.Edit -> NavEntry(MediaSendNavKey.Edit) {
is MediaSendRoute.Edit -> NavEntry(MediaSendRoute.Edit) {
val state by viewModel.state.collectAsStateWithLifecycle()
MediaEditScreen(
state = state,
@@ -114,7 +114,7 @@ internal fun MediaSendNavigation(
)
}
is MediaSendNavKey.Send -> NavEntry(key) {
is MediaSendRoute.Send -> NavEntry(key) {
val state by viewModel.state.collectAsStateWithLifecycle()
sendSlot(state)
}
@@ -10,12 +10,12 @@ import kotlinx.serialization.Serializable
import org.signal.core.models.media.MediaFolder
/**
* Nav3 keys
* Using @Serializable and NavKey for type-safe navigation with Navigation 3.
*/
@Serializable
sealed interface MediaSendNavKey : NavKey {
sealed interface MediaSendRoute : NavKey {
@Serializable
sealed interface Select : MediaSendNavKey {
sealed interface Select : MediaSendRoute {
@Serializable
data object Folders : Select
@@ -24,7 +24,7 @@ sealed interface MediaSendNavKey : NavKey {
}
@Serializable
sealed interface Capture : MediaSendNavKey {
sealed interface Capture : MediaSendRoute {
@Serializable
data object Chrome : Capture
@@ -36,8 +36,8 @@ sealed interface MediaSendNavKey : NavKey {
}
@Serializable
data object Edit : MediaSendNavKey
data object Edit : MediaSendRoute
@Serializable
data object Send : MediaSendNavKey
data object Send : MediaSendRoute
}
@@ -27,7 +27,7 @@ fun MediaSendScreen(
modifier: Modifier = Modifier,
textStoryEditorSlot: @Composable () -> Unit = {},
sendSlot: @Composable (MediaSendFlowState) -> Unit = {},
onExternalHudCommand: (HudCommand) -> Unit = {}
onExternalHudCommand: (MediaSendFlowHudCommand) -> Unit = {}
) {
val viewModel = viewModel<MediaSendFlowViewModel>(factory = MediaSendFlowViewModel.Factory(args = contractArgs))
@@ -10,27 +10,27 @@ import androidx.navigation3.runtime.NavKey
import org.signal.core.models.media.MediaFolder
internal fun NavBackStack<NavKey>.goToEdit() {
goToSingle(MediaSendNavKey.Edit)
goToSingle(MediaSendRoute.Edit)
}
internal fun NavBackStack<NavKey>.goToSend() {
goToSingle(MediaSendNavKey.Send)
goToSingle(MediaSendRoute.Send)
}
internal fun NavBackStack<NavKey>.goToFolders() {
goToSingle(MediaSendNavKey.Select.Folders)
goToSingle(MediaSendRoute.Select.Folders)
}
internal fun NavBackStack<NavKey>.goToFiles(mediaFolder: MediaFolder) {
add(MediaSendNavKey.Select.Files(mediaFolder))
add(MediaSendRoute.Select.Files(mediaFolder))
}
internal fun NavBackStack<NavKey>.goToTextStory() {
goToSingle(MediaSendNavKey.Capture.TextStory)
goToSingle(MediaSendRoute.Capture.TextStory)
}
internal fun NavBackStack<NavKey>.goToCamera() {
goToSingle(MediaSendNavKey.Capture.Camera)
goToSingle(MediaSendRoute.Capture.Camera)
}
/**
@@ -335,17 +335,17 @@ internal fun rememberVideoRecordingConfig(mediaConstraints: MediaConstraints?, m
}
/**
* Bridges [CameraXScreenEvent]s emitted by [CameraXScreen] back onto the legacy [CameraFragment.Controller] callbacks
* Bridges [CameraXScreenEvents]s emitted by [CameraXScreen] back onto the legacy [CameraFragment.Controller] callbacks
* for Fragment-based consumers.
*/
private fun CameraFragment.Controller.onCameraXScreenEvent(event: CameraXScreenEvent) {
private fun CameraFragment.Controller.onCameraXScreenEvent(event: CameraXScreenEvents) {
when (event) {
is CameraXScreenEvent.ImageCaptured -> onImageCaptured(event.data, event.width, event.height)
is CameraXScreenEvent.VideoCaptured -> onVideoCaptured(event.fd, event.durationMs)
is CameraXScreenEvent.QrCodeFound -> onQrCodeFound(event.data)
CameraXScreenEvent.VideoCaptureError -> onVideoCaptureError()
CameraXScreenEvent.GalleryClicked -> onGalleryClicked()
CameraXScreenEvent.CameraCloseClicked -> onCameraCloseClicked()
is CameraXScreenEvents.ImageCaptured -> onImageCaptured(event.data, event.width, event.height)
is CameraXScreenEvents.VideoCaptured -> onVideoCaptured(event.fd, event.durationMs)
is CameraXScreenEvents.QrCodeFound -> onQrCodeFound(event.data)
CameraXScreenEvents.VideoCaptureError -> onVideoCaptureError()
CameraXScreenEvents.GalleryClicked -> onGalleryClicked()
CameraXScreenEvents.CameraCloseClicked -> onCameraCloseClicked()
}
}
@@ -438,7 +438,7 @@ class VideoFileDescriptor(val context: Context) {
@Composable
fun CameraXScreen(
state: CameraXScreenState,
onEvent: (CameraXScreenEvent) -> Unit,
onEvent: (CameraXScreenEvents) -> Unit,
videoRecordingConfig: VideoRecordingConfig,
onCheckPermissions: () -> Unit,
hasCameraPermission: () -> Boolean,
@@ -499,7 +499,7 @@ fun CameraXScreen(
LaunchedEffect(cameraViewModel, state.isQrScanEnabled) {
if (state.isQrScanEnabled) {
cameraViewModel.qrCodeDetected.collect { qrCode ->
onEvent(CameraXScreenEvent.QrCodeFound(qrCode))
onEvent(CameraXScreenEvents.QrCodeFound(qrCode))
}
}
}
@@ -629,7 +629,7 @@ fun CameraXScreen(
PermissionMissingContent(
isVideoEnabled = captureMode != CameraCaptureMode.ImageOnly,
onRequestPermissions = onCheckPermissions,
onGalleryClicked = { onEvent(CameraXScreenEvent.GalleryClicked) },
onGalleryClicked = { onEvent(CameraXScreenEvents.GalleryClicked) },
galleryButtonBottomPadding = hudBottomMargin + 16.dp
)
}
@@ -687,7 +687,7 @@ private fun handleHudEvent(
event: StandardCameraHudEvents,
context: Context,
cameraViewModel: CameraScreenViewModel,
onEvent: (CameraXScreenEvent) -> Unit,
onEvent: (CameraXScreenEvents) -> Unit,
isVideoEnabled: Boolean,
onRequestMicPermission: () -> Unit,
createVideoFileDescriptor: () -> ActiveRecording?,
@@ -725,11 +725,11 @@ private fun handleHudEvent(
}
is StandardCameraHudEvents.GalleryClick -> {
onEvent(CameraXScreenEvent.GalleryClicked)
onEvent(CameraXScreenEvents.GalleryClicked)
}
is StandardCameraHudEvents.CloseClick -> {
onEvent(CameraXScreenEvent.CameraCloseClicked)
onEvent(CameraXScreenEvents.CameraCloseClicked)
}
is StandardCameraHudEvents.ToggleFlash -> {
@@ -754,33 +754,33 @@ private fun handleHudEvent(
}
}
private fun handlePhotoCaptured(bitmap: Bitmap, onEvent: (CameraXScreenEvent) -> Unit) {
private fun handlePhotoCaptured(bitmap: Bitmap, onEvent: (CameraXScreenEvents) -> Unit) {
// Convert bitmap to JPEG byte array
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
val data = outputStream.toByteArray()
onEvent(CameraXScreenEvent.ImageCaptured(data, bitmap.width, bitmap.height))
onEvent(CameraXScreenEvents.ImageCaptured(data, bitmap.width, bitmap.height))
}
private fun handleVideoCaptured(
result: VideoCaptureResult,
releaseVideoFileDescriptor: () -> SeekableFileDescriptor?,
onEvent: (CameraXScreenEvent) -> Unit
onEvent: (CameraXScreenEvents) -> Unit
) {
when (result) {
is VideoCaptureResult.Success -> {
val descriptor = releaseVideoFileDescriptor()
if (descriptor != null) {
onEvent(CameraXScreenEvent.VideoCaptured(descriptor, result.durationMs))
onEvent(CameraXScreenEvents.VideoCaptured(descriptor, result.durationMs))
} else {
onEvent(CameraXScreenEvent.VideoCaptureError)
onEvent(CameraXScreenEvents.VideoCaptureError)
}
}
is VideoCaptureResult.Error -> {
Log.w(TAG, "Video capture failed: ${result.message}", result.throwable)
onEvent(CameraXScreenEvent.VideoCaptureError)
onEvent(CameraXScreenEvents.VideoCaptureError)
}
}
}
@@ -7,16 +7,16 @@ package org.signal.mediasend.screens.capture
import org.signal.core.util.SeekableFileDescriptor
sealed interface CameraXScreenEvent {
class ImageCaptured(val data: ByteArray, val width: Int, val height: Int) : CameraXScreenEvent
sealed interface CameraXScreenEvents {
class ImageCaptured(val data: ByteArray, val width: Int, val height: Int) : CameraXScreenEvents
/**
* @param fd Owned by the consumer, which must close it once it is finished reading the recording.
* @param durationMs How long the recording ran, as reported by the recorder.
*/
class VideoCaptured(val fd: SeekableFileDescriptor, val durationMs: Long) : CameraXScreenEvent
class QrCodeFound(val data: String) : CameraXScreenEvent
data object VideoCaptureError : CameraXScreenEvent
data object GalleryClicked : CameraXScreenEvent
data object CameraCloseClicked : CameraXScreenEvent
class VideoCaptured(val fd: SeekableFileDescriptor, val durationMs: Long) : CameraXScreenEvents
class QrCodeFound(val data: String) : CameraXScreenEvents
data object VideoCaptureError : CameraXScreenEvents
data object GalleryClicked : CameraXScreenEvents
data object CameraCloseClicked : CameraXScreenEvents
}
@@ -18,7 +18,7 @@ import org.signal.mediasend.rememberPreviewState
@Composable
fun MediaCameraCaptureScreen(
state: MediaSendFlowState,
onEvent: (MediaCaptureScreenEvent) -> Unit
onEvent: (MediaCaptureScreenEvents) -> Unit
) {
CameraXScreen(
state = remember(state.selectedMedia) {
@@ -28,7 +28,7 @@ fun MediaCameraCaptureScreen(
selectedMediaCount = state.selectedMedia.size
)
},
onEvent = { event -> onEvent(MediaCaptureScreenEvent.Camera(event)) },
onEvent = { event -> onEvent(MediaCaptureScreenEvents.Camera(event)) },
videoRecordingConfig = rememberVideoRecordingConfig(
mediaConstraints = state.mediaConstraints,
maxDurationSecondsOverride = if (state.isStory) state.storyMaxVideoDuration.inWholeSeconds.toInt() else 0
@@ -56,7 +56,7 @@ import org.signal.glide.compose.GlideImage
import org.signal.glide.decryptableuri.DecryptableUri
import org.signal.mediasend.MediaSendFlowActivityContract
import org.signal.mediasend.MediaSendFlowState
import org.signal.mediasend.MediaSendNavKey
import org.signal.mediasend.MediaSendRoute
import org.signal.mediasend.R
import org.signal.mediasend.rememberPreviewState
import org.signal.mediasend.screens.edit.rememberPreviewMedia
@@ -72,9 +72,9 @@ private const val TEXT_STORY_Z_INDEX = 1f
*/
@Composable
fun MediaCaptureScreen(
selectedCaptureScreen: MediaSendNavKey.Capture,
selectedCaptureScreen: MediaSendRoute.Capture,
state: MediaSendFlowState,
onEvent: (MediaCaptureScreenEvent) -> Unit,
onEvent: (MediaCaptureScreenEvents) -> Unit,
textStoryEditorSlot: @Composable () -> Unit
) {
Box(
@@ -85,7 +85,7 @@ fun MediaCaptureScreen(
AnimatedContent(
targetState = selectedCaptureScreen,
transitionSpec = {
if (targetState is MediaSendNavKey.Capture.TextStory) {
if (targetState is MediaSendRoute.Capture.TextStory) {
ContentTransform(
targetContentEnter = slideInHorizontally { width -> width },
initialContentExit = ExitTransition.KeepUntilTransitionsFinished,
@@ -101,7 +101,7 @@ fun MediaCaptureScreen(
}
) { captureScreen ->
when (captureScreen) {
is MediaSendNavKey.Capture.TextStory -> textStoryEditorSlot()
is MediaSendRoute.Capture.TextStory -> textStoryEditorSlot()
else -> {
MediaCameraCaptureScreen(
state = state,
@@ -139,9 +139,9 @@ private fun rememberCanDisplayBottomBar(state: MediaSendFlowState): Boolean {
fun MediaCaptureBottomBar(
canDisplayToggleSwitch: Boolean,
canDisplayMediaBar: Boolean,
selectedCaptureScreen: MediaSendNavKey.Capture,
selectedCaptureScreen: MediaSendRoute.Capture,
selectedMedia: List<Media>,
onEvent: (MediaCaptureScreenEvent) -> Unit,
onEvent: (MediaCaptureScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
if (canDisplayToggleSwitch) {
@@ -161,8 +161,8 @@ fun MediaCaptureBottomBar(
@Composable
private fun MediaCaptureToggleBar(
selectedCaptureScreen: MediaSendNavKey.Capture,
onEvent: (MediaCaptureScreenEvent) -> Unit,
selectedCaptureScreen: MediaSendRoute.Capture,
onEvent: (MediaCaptureScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val cameraDisplay = CameraDisplay.rememberCameraDisplay(isLandscape = false)
@@ -175,15 +175,15 @@ private fun MediaCaptureToggleBar(
.padding(horizontal = 6.dp, vertical = 6.dp)
) {
SegmentedBarButton(
selected = selectedCaptureScreen == MediaSendNavKey.Capture.Camera,
onClick = { onEvent(MediaCaptureScreenEvent.ShowCamera) }
selected = selectedCaptureScreen == MediaSendRoute.Capture.Camera,
onClick = { onEvent(MediaCaptureScreenEvents.ShowCamera) }
) {
Text(text = stringResource(R.string.MediaCaptureScreen__camera))
}
SegmentedBarButton(
selected = selectedCaptureScreen == MediaSendNavKey.Capture.TextStory,
onClick = { onEvent(MediaCaptureScreenEvent.ShowTextStory) }
selected = selectedCaptureScreen == MediaSendRoute.Capture.TextStory,
onClick = { onEvent(MediaCaptureScreenEvents.ShowTextStory) }
) {
Text(text = stringResource(R.string.MediaCaptureScreen__text))
}
@@ -213,7 +213,7 @@ private fun SingleChoiceSegmentedButtonRowScope.SegmentedBarButton(
@Composable
private fun MediaCaptureMediaBar(
selectedMedia: List<Media>,
onEvent: (MediaCaptureScreenEvent) -> Unit,
onEvent: (MediaCaptureScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val cameraDisplay = CameraDisplay.rememberCameraDisplay(isLandscape = false)
@@ -260,13 +260,13 @@ private fun MediaCaptureMediaBar(
@Composable
private fun NextButton(
onEvent: (MediaCaptureScreenEvent) -> Unit,
onEvent: (MediaCaptureScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val cameraDisplay = CameraDisplay.rememberCameraDisplay(isLandscape = false)
IconButton(
onClick = { onEvent(MediaCaptureScreenEvent.NextClicked) },
onClick = { onEvent(MediaCaptureScreenEvents.NextClicked) },
modifier = modifier
.padding(bottom = cameraDisplay.getNextPaddingBottom().dp, end = cameraDisplay.getNextPaddingEnd().dp)
.size(48.dp)
@@ -286,7 +286,7 @@ private fun NextButton(
fun MediaCaptureScreenPreview() {
Previews.Preview {
MediaCaptureScreen(
selectedCaptureScreen = MediaSendNavKey.Capture.Camera,
selectedCaptureScreen = MediaSendRoute.Capture.Camera,
state = rememberPreviewState()
.copy(
isCameraFirst = true,
@@ -306,7 +306,7 @@ fun MediaCaptureScreenWithSelectedMediaPreview() {
Previews.Preview {
MediaCaptureScreen(
selectedCaptureScreen = MediaSendNavKey.Capture.Camera,
selectedCaptureScreen = MediaSendRoute.Capture.Camera,
state = rememberPreviewState()
.copy(
isCameraFirst = true,
@@ -323,15 +323,15 @@ fun MediaCaptureScreenWithSelectedMediaPreview() {
@NightPreview
@Composable
fun MediaCaptureToggleBarPreview() {
var selectedCaptureScreen: MediaSendNavKey.Capture by remember { mutableStateOf(MediaSendNavKey.Capture.Camera) }
var selectedCaptureScreen: MediaSendRoute.Capture by remember { mutableStateOf(MediaSendRoute.Capture.Camera) }
Previews.Preview {
MediaCaptureToggleBar(
selectedCaptureScreen = selectedCaptureScreen,
onEvent = {
when (it) {
MediaCaptureScreenEvent.ShowCamera -> selectedCaptureScreen = MediaSendNavKey.Capture.Camera
MediaCaptureScreenEvent.ShowTextStory -> selectedCaptureScreen = MediaSendNavKey.Capture.TextStory
MediaCaptureScreenEvents.ShowCamera -> selectedCaptureScreen = MediaSendRoute.Capture.Camera
MediaCaptureScreenEvents.ShowTextStory -> selectedCaptureScreen = MediaSendRoute.Capture.TextStory
else -> Unit
}
}
@@ -342,7 +342,7 @@ fun MediaCaptureToggleBarPreview() {
@NightPreview
@Composable
fun MediaCaptureMediaBarPreview() {
var selectedCaptureScreen: MediaSendNavKey.Capture by remember { mutableStateOf(MediaSendNavKey.Capture.Camera) }
var selectedCaptureScreen: MediaSendRoute.Capture by remember { mutableStateOf(MediaSendRoute.Capture.Camera) }
val selectedMedia = rememberPreviewMedia(1)
Previews.Preview {
@@ -350,8 +350,8 @@ fun MediaCaptureMediaBarPreview() {
selectedMedia = selectedMedia,
onEvent = {
when (it) {
MediaCaptureScreenEvent.ShowCamera -> selectedCaptureScreen = MediaSendNavKey.Capture.Camera
MediaCaptureScreenEvent.ShowTextStory -> selectedCaptureScreen = MediaSendNavKey.Capture.TextStory
MediaCaptureScreenEvents.ShowCamera -> selectedCaptureScreen = MediaSendRoute.Capture.Camera
MediaCaptureScreenEvents.ShowTextStory -> selectedCaptureScreen = MediaSendRoute.Capture.TextStory
else -> Unit
}
}
@@ -1,15 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.mediasend.screens.capture
sealed interface MediaCaptureScreenEvent {
data object ShowCamera : MediaCaptureScreenEvent
data object ShowTextStory : MediaCaptureScreenEvent
data object NextClicked : MediaCaptureScreenEvent
data object CycleTextStoryBackgroundColor : MediaCaptureScreenEvent
data object AddLinkToTextStory : MediaCaptureScreenEvent
class Camera(val event: CameraXScreenEvent) : MediaCaptureScreenEvent
}
@@ -0,0 +1,15 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.mediasend.screens.capture
sealed interface MediaCaptureScreenEvents {
data object ShowCamera : MediaCaptureScreenEvents
data object ShowTextStory : MediaCaptureScreenEvents
data object NextClicked : MediaCaptureScreenEvents
data object CycleTextStoryBackgroundColor : MediaCaptureScreenEvents
data object AddLinkToTextStory : MediaCaptureScreenEvents
class Camera(val event: CameraXScreenEvents) : MediaCaptureScreenEvents
}
@@ -31,7 +31,7 @@ import org.signal.mediasend.R
@Composable
fun TextStoryHorizontalBar(
background: Brush,
onEvent: (MediaCaptureScreenEvent) -> Unit,
onEvent: (MediaCaptureScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
Row(
@@ -48,7 +48,7 @@ fun TextStoryHorizontalBar(
@Composable
fun TextStoryVerticalBar(
background: Brush,
onEvent: (MediaCaptureScreenEvent) -> Unit,
onEvent: (MediaCaptureScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
Column(
@@ -65,12 +65,12 @@ fun TextStoryVerticalBar(
@Composable
private fun ColorButton(
background: Brush,
onEvent: (MediaCaptureScreenEvent) -> Unit
onEvent: (MediaCaptureScreenEvents) -> Unit
) {
IconButtons.IconButton(
size = 48.dp,
onClick = {
onEvent(MediaCaptureScreenEvent.CycleTextStoryBackgroundColor)
onEvent(MediaCaptureScreenEvents.CycleTextStoryBackgroundColor)
}
) {
Box(
@@ -85,12 +85,12 @@ private fun ColorButton(
@Composable
private fun LinkButton(
onEvent: (MediaCaptureScreenEvent) -> Unit
onEvent: (MediaCaptureScreenEvents) -> Unit
) {
IconButtons.IconButton(
size = 48.dp,
onClick = {
onEvent(MediaCaptureScreenEvent.AddLinkToTextStory)
onEvent(MediaCaptureScreenEvents.AddLinkToTextStory)
}
) {
Icon(
@@ -62,7 +62,7 @@ val LocalAddAMessageRowTextField = compositionLocalOf<@Composable (CharSequence,
@Composable
fun AddAMessageRow(
message: CharSequence?,
onEvent: (MediaEditScreenEvent) -> Unit,
onEvent: (MediaEditScreenEvents) -> Unit,
onNextClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
@@ -87,7 +87,7 @@ fun AddAMessageRow(
// A view-once send cannot carry a body, so the row becomes a static label rather than an entry point.
Modifier
} else {
Modifier.clickable(enabled = enabled, onClickLabel = stringResource(R.string.AddAMessageRow__add_a_message), onClick = { onEvent(MediaEditScreenEvent.AddMessageClick()) }, role = Role.Button)
Modifier.clickable(enabled = enabled, onClickLabel = stringResource(R.string.AddAMessageRow__add_a_message), onClick = { onEvent(MediaEditScreenEvents.AddMessageClick()) }, role = Role.Button)
}
)
) {
@@ -104,7 +104,7 @@ fun AddAMessageRow(
} else {
IconButtons.IconButton(
enabled = enabled,
onClick = { onEvent(MediaEditScreenEvent.AddMessageClick(startWithEmojiKeyboard = true)) }
onClick = { onEvent(MediaEditScreenEvents.AddMessageClick(startWithEmojiKeyboard = true)) }
) {
Icon(
painter = SignalIcons.Emoji.painter,
@@ -123,7 +123,7 @@ fun AddAMessageRow(
if (viewOnceAvailable) {
IconButtons.IconButton(
enabled = enabled,
onClick = { onEvent(MediaEditScreenEvent.ToggleViewOnce) }
onClick = { onEvent(MediaEditScreenEvents.ToggleViewOnce) }
) {
Icon(
painter = if (viewOnce) SignalIcons.ViewOnce.painter else SignalIcons.ViewOnceInfinite.painter,
@@ -159,7 +159,7 @@ fun AddAMessageRow(
ScheduleSendMenu(
controller = scheduleSendMenuController,
onOptionClick = { onEvent(MediaEditScreenEvent.ScheduleSendClick(it)) }
onOptionClick = { onEvent(MediaEditScreenEvents.ScheduleSendClick(it)) }
)
}
}
@@ -72,7 +72,7 @@ import org.signal.mediasend.screens.edit.video.VideoTrimBar
@Composable
internal fun MediaEditScreen(
state: MediaSendFlowState,
onEvent: (MediaEditScreenEvent) -> Unit,
onEvent: (MediaEditScreenEvents) -> Unit,
imageControllers: ImageController.Container
) {
val scope = rememberCoroutineScope()
@@ -97,7 +97,7 @@ internal fun MediaEditScreen(
state.selectedMedia.size == 1 &&
state.selectedMedia.firstOrNull() == state.cameraFirstCapture
BackHandler(enabled = isOnlyCameraFirstCapture) {
onEvent(MediaEditScreenEvent.NavigateBack)
onEvent(MediaEditScreenEvents.NavigateBack)
}
Box(modifier = Modifier.fillMaxSize()) {
@@ -224,7 +224,7 @@ internal fun MediaEditScreen(
imageController.setBrushWidthFraction(fraction)
if (gestureComplete && tool != null) {
onEvent(MediaEditScreenEvent.BrushWidthChanged(tool, fraction))
onEvent(MediaEditScreenEvents.BrushWidthChanged(tool, fraction))
}
}
)
@@ -266,11 +266,11 @@ internal fun MediaEditScreen(
pagerState = pagerState,
enabled = !isInteracting,
onFocusedMediaChange = {
onEvent(MediaEditScreenEvent.FocusedMediaChanged(it))
onEvent(MediaEditScreenEvents.FocusedMediaChanged(it))
},
onThumbnailClick = { index ->
if (pagerState.currentPage == index) {
onEvent(MediaEditScreenEvent.RemoveMedia(state.selectedMedia[index]))
onEvent(MediaEditScreenEvents.RemoveMedia(state.selectedMedia[index]))
} else {
scope.launch {
pagerState.animateScrollToPage(index)
@@ -278,7 +278,7 @@ internal fun MediaEditScreen(
}
},
onReorder = { fromIndex, toIndex ->
onEvent(MediaEditScreenEvent.ReorderSelectedMedia(fromIndex, toIndex))
onEvent(MediaEditScreenEvents.ReorderSelectedMedia(fromIndex, toIndex))
}
)
}
@@ -304,7 +304,7 @@ internal fun MediaEditScreen(
MediaEditControl(faded = isDragging) {
BlurFacesBar(
checked = controller.isBlurringFaces,
onCheckedChange = { onEvent(MediaEditScreenEvent.ToggleBlurFaces(it)) }
onCheckedChange = { onEvent(MediaEditScreenEvents.ToggleBlurFaces(it)) }
)
}
}
@@ -337,7 +337,7 @@ internal fun MediaEditScreen(
message = state.message,
recipientChatColor = recipientChatColor,
onEvent = onEvent,
onNextClick = { onEvent(MediaEditScreenEvent.NextClick) },
onNextClick = { onEvent(MediaEditScreenEvents.NextClick) },
modifier = Modifier
.widthIn(max = 624.dp)
.padding(horizontal = 16.dp)
@@ -410,7 +410,7 @@ internal fun MediaEditScreen(
@Composable
private fun MediaToolbar(
state: MediaSendFlowState,
onEvent: (MediaEditScreenEvent) -> Unit,
onEvent: (MediaEditScreenEvents) -> Unit,
focusedUri: Uri?,
focusedEditorState: EditorState?,
imageController: ImageController?,
@@ -465,7 +465,7 @@ private fun VideoTrimTimeline(
editorState: EditorState.VideoTrim,
videoEditorViewModel: VideoEditorViewModel,
onInteractingChange: (Boolean) -> Unit,
onEvent: (MediaEditScreenEvent) -> Unit
onEvent: (MediaEditScreenEvents) -> Unit
) {
val playbackPositionUs by produceState(editorState.videoTrimData.startTimeUs, videoUri) {
videoEditorViewModel.events(videoUri).collect { event ->
@@ -483,12 +483,12 @@ private fun VideoTrimTimeline(
playbackPositionUs = playbackPositionUs,
onEvent = { event ->
when (event) {
is MediaEditScreenEvent.VideoTrimChanged -> {
is MediaEditScreenEvents.VideoTrimChanged -> {
onInteractingChange(!event.editingComplete)
onEvent(event)
}
is MediaEditScreenEvent.VideoSeek -> {
is MediaEditScreenEvents.VideoSeek -> {
onInteractingChange(!event.editingComplete)
videoEditorViewModel.sendCommand(
videoUri,
@@ -10,21 +10,21 @@ import org.signal.mediasend.SentMediaQuality
import org.signal.mediasend.screens.edit.image.BrushTool
import org.signal.mediasend.screens.edit.video.VideoTrimData
sealed interface MediaEditScreenEvent {
data class RemoveMedia(val media: Media) : MediaEditScreenEvent
data class FocusedMediaChanged(val media: Media) : MediaEditScreenEvent
data class ReorderSelectedMedia(val fromIndex: Int, val toIndex: Int) : MediaEditScreenEvent
data class AddMessageClick(val startWithEmojiKeyboard: Boolean = false) : MediaEditScreenEvent
data object StickerClick : MediaEditScreenEvent
data object NextClick : MediaEditScreenEvent
data class ScheduleSendClick(val option: ScheduleSendOption) : MediaEditScreenEvent
data object NavigateBack : MediaEditScreenEvent
data object NavigateToGallery : MediaEditScreenEvent
data class SetMediaQuality(val quality: SentMediaQuality) : MediaEditScreenEvent
data object ToggleViewOnce : MediaEditScreenEvent
data class BrushWidthChanged(val tool: BrushTool, val fraction: Float) : MediaEditScreenEvent
data class ToggleBlurFaces(val enabled: Boolean) : MediaEditScreenEvent
data object SaveMedia : MediaEditScreenEvent
data class VideoTrimChanged(val videoTrimData: VideoTrimData, val editingComplete: Boolean) : MediaEditScreenEvent
data class VideoSeek(val positionUs: Long, val editingComplete: Boolean) : MediaEditScreenEvent
sealed interface MediaEditScreenEvents {
data class RemoveMedia(val media: Media) : MediaEditScreenEvents
data class FocusedMediaChanged(val media: Media) : MediaEditScreenEvents
data class ReorderSelectedMedia(val fromIndex: Int, val toIndex: Int) : MediaEditScreenEvents
data class AddMessageClick(val startWithEmojiKeyboard: Boolean = false) : MediaEditScreenEvents
data object StickerClick : MediaEditScreenEvents
data object NextClick : MediaEditScreenEvents
data class ScheduleSendClick(val option: ScheduleSendOption) : MediaEditScreenEvents
data object NavigateBack : MediaEditScreenEvents
data object NavigateToGallery : MediaEditScreenEvents
data class SetMediaQuality(val quality: SentMediaQuality) : MediaEditScreenEvents
data object ToggleViewOnce : MediaEditScreenEvents
data class BrushWidthChanged(val tool: BrushTool, val fraction: Float) : MediaEditScreenEvents
data class ToggleBlurFaces(val enabled: Boolean) : MediaEditScreenEvents
data object SaveMedia : MediaEditScreenEvents
data class VideoTrimChanged(val videoTrimData: VideoTrimData, val editingComplete: Boolean) : MediaEditScreenEvents
data class VideoSeek(val positionUs: Long, val editingComplete: Boolean) : MediaEditScreenEvents
}
@@ -97,7 +97,7 @@ internal fun MediaEditorToolbarButton(
internal fun MediaEditorToolbarSharedButtons(
state: MediaSendFlowState,
editorState: EditorState,
onEvent: (MediaEditScreenEvent) -> Unit
onEvent: (MediaEditScreenEvents) -> Unit
) {
if (isQualityVisible(state, editorState)) {
var isSelectingQuality by rememberSaveable { mutableStateOf(false) }
@@ -105,7 +105,7 @@ internal fun MediaEditorToolbarSharedButtons(
if (isSelectingQuality) {
QualitySelectorBottomSheet(
quality = state.sentMediaQuality,
onQualitySelected = { onEvent(MediaEditScreenEvent.SetMediaQuality(it)) },
onQualitySelected = { onEvent(MediaEditScreenEvents.SetMediaQuality(it)) },
onDismiss = { isSelectingQuality = false }
)
}
@@ -124,7 +124,7 @@ internal fun MediaEditorToolbarSharedButtons(
if (isSaveVisible(editorState)) {
MediaEditorToolbarButton(
imageVector = SignalIcons.Save.imageVector,
onClick = { onEvent(MediaEditScreenEvent.SaveMedia) },
onClick = { onEvent(MediaEditScreenEvents.SaveMedia) },
modifier = Modifier.testTag(TestTags.MEDIA_EDITOR_TOOLBAR_SAVE_BUTTON)
)
}
@@ -132,7 +132,7 @@ internal fun MediaEditorToolbarSharedButtons(
if (isAddMediaVisible(state, editorState)) {
MediaEditorToolbarButton(
imageVector = SignalIcons.Plus.imageVector, // TODO [alex] - wrong art asset
onClick = { onEvent(MediaEditScreenEvent.NavigateToGallery) },
onClick = { onEvent(MediaEditScreenEvents.NavigateToGallery) },
modifier = Modifier.testTag(TestTags.MEDIA_EDITOR_TOOLBAR_ADD_MEDIA_BUTTON)
)
}
@@ -30,7 +30,7 @@ import org.signal.mediasend.MediaSendFlowState
import org.signal.mediasend.rememberPreviewState
import org.signal.mediasend.screens.edit.ImageController
import org.signal.mediasend.screens.edit.MediaEditScreenDialogs
import org.signal.mediasend.screens.edit.MediaEditScreenEvent
import org.signal.mediasend.screens.edit.MediaEditScreenEvents
import org.signal.mediasend.screens.edit.MediaEditorToolbar
import org.signal.mediasend.screens.edit.MediaEditorToolbarButton
import org.signal.mediasend.screens.edit.MediaEditorToolbarSharedButtons
@@ -41,7 +41,7 @@ internal fun ImageEditorToolbar(
imageEditorController: ImageController,
state: MediaSendFlowState,
editorState: EditorState.Image,
onEvent: (MediaEditScreenEvent) -> Unit,
onEvent: (MediaEditScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
when {
@@ -71,7 +71,7 @@ private fun ImageEditorNoneStateToolbar(
imageEditorController: ImageController,
state: MediaSendFlowState,
editorState: EditorState.Image,
onEvent: (MediaEditScreenEvent) -> Unit,
onEvent: (MediaEditScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
MediaEditorToolbar(modifier) {
@@ -96,7 +96,7 @@ private fun ImageEditorNoneStateToolbar(
@Composable
private fun ImageEditorDrawStateToolbar(
imageEditorController: ImageController,
onEvent: (MediaEditScreenEvent) -> Unit,
onEvent: (MediaEditScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
MediaEditorToolbar(
@@ -135,7 +135,7 @@ private fun ImageEditorDrawStateToolbar(
// Unconditional: if a previous pick never delivered a result the mode is still INSERT_STICKER, and gating on it
// would leave the button dead.
imageEditorController.enterStickerMode()
onEvent(MediaEditScreenEvent.StickerClick)
onEvent(MediaEditScreenEvents.StickerClick)
}
)
@@ -26,7 +26,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.util.logging.Log
import org.signal.mediasend.R
import org.signal.mediasend.screens.edit.MediaEditScreenEvent
import org.signal.mediasend.screens.edit.MediaEditScreenEvents
import org.thoughtcrime.securesms.video.interfaces.MediaInputFactory
import java.io.IOException
import java.util.concurrent.TimeUnit
@@ -36,8 +36,8 @@ private const val TAG = "VideoEditorToolbar"
/**
* Timeline/trim toolbar for a video. Trim data and the current playback position are owned elsewhere and passed in;
* all user-driven changes are reported back through [onEvent] [MediaEditScreenEvent.VideoTrimChanged] for trim
* handles and [MediaEditScreenEvent.VideoSeek] for scrubbing the playback position.
* all user-driven changes are reported back through [onEvent] [MediaEditScreenEvents.VideoTrimChanged] for trim
* handles and [MediaEditScreenEvents.VideoSeek] for scrubbing the playback position.
*
* [videoUri] and [mediaInputFactory] are required to load the underlying video: the timeline renders nothing until an
* input is set, since that is also what drives thumbnail extraction and duration discovery. [maxSelectableDurationUs]
@@ -50,7 +50,7 @@ fun VideoTrimBar(
videoTrimData: VideoTrimData,
maxSelectableDurationUs: Long = 0L,
playbackPositionUs: Long = 0L,
onEvent: (MediaEditScreenEvent) -> Unit = {}
onEvent: (MediaEditScreenEvents) -> Unit = {}
) {
val currentOnEvent by rememberUpdatedState(onEvent)
var isDragging by remember { mutableStateOf(false) }
@@ -60,12 +60,12 @@ fun VideoTrimBar(
object : VideoThumbnailsRangeSelectorView.PositionDragListener {
override fun onPositionDrag(position: Long) {
isDragging = true
currentOnEvent(MediaEditScreenEvent.VideoSeek(positionUs = position, editingComplete = false))
currentOnEvent(MediaEditScreenEvents.VideoSeek(positionUs = position, editingComplete = false))
}
override fun onEndPositionDrag(position: Long) {
isDragging = false
currentOnEvent(MediaEditScreenEvent.VideoSeek(positionUs = position, editingComplete = true))
currentOnEvent(MediaEditScreenEvents.VideoSeek(positionUs = position, editingComplete = true))
}
}
}
@@ -74,7 +74,7 @@ fun VideoTrimBar(
VideoThumbnailsRangeSelectorView.RangeDragListener { minValue, maxValue, duration, end ->
isDragging = !end
currentOnEvent(
MediaEditScreenEvent.VideoTrimChanged(
MediaEditScreenEvents.VideoTrimChanged(
videoTrimData = VideoTrimData(
isDurationEdited = minValue > 0 || maxValue < duration,
totalInputDurationUs = duration,
@@ -107,15 +107,15 @@ private const val PLACEHOLDER_COUNT = 100
*/
@Composable
internal fun MediaSelectScreen(
state: MediaSelectScreenState,
onEvent: (MediaSelectScreenEvent) -> Unit
state: MediaSelectState,
onEvent: (MediaSelectScreenEvents) -> Unit
) {
// Without read access there is nothing to browse, and with selected-photos access and nothing selected there is
// nothing yet. Both show the placeholder grid behind a call to action, so both use the denser file grid.
val showPlaceholders = state.mediaPermissions == MediaPermissions.NONE ||
(state.mediaPermissions == MediaPermissions.PARTIAL && !state.hasContent)
val gridConfiguration = rememberGridConfiguration(state is MediaSelectScreenState.Folders && !showPlaceholders)
val gridConfiguration = rememberGridConfiguration(state is MediaSelectState.Folders && !showPlaceholders)
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val gridState = rememberLazyGridState()
@@ -126,27 +126,27 @@ internal fun MediaSelectScreen(
LaunchedEffect(state.isSelectionRejected) {
if (state.isSelectionRejected) {
dragToSelectState.cancel()
onEvent(MediaSelectScreenEvent.SelectionRejectionShown)
onEvent(MediaSelectScreenEvents.SelectionRejectionShown)
}
}
// The system prompt and the app settings round-trip both land us back here, and neither tells us what changed.
val currentOnEvent by rememberUpdatedState(onEvent)
LifecycleResumeEffect(Unit) {
currentOnEvent(MediaSelectScreenEvent.Refresh)
currentOnEvent(MediaSelectScreenEvents.Refresh)
onPauseOrDispose { }
}
Scaffolds.Settings(
title = when (state) {
is MediaSelectScreenState.Folders -> stringResource(R.string.MediaSelectScreen__gallery)
is MediaSelectScreenState.Files -> state.selectedMediaFolder.title
is MediaSelectState.Folders -> stringResource(R.string.MediaSelectScreen__gallery)
is MediaSelectState.Files -> state.selectedMediaFolder.title
},
navigationIcon = ImageVector.vectorResource(org.signal.core.ui.R.drawable.symbol_arrow_start_24),
onNavigationClick = { backDispatcher?.onBackPressed() },
actions = {
IconButton(onClick = {
onEvent(MediaSelectScreenEvent.NavigateToCamera)
onEvent(MediaSelectScreenEvents.NavigateToCamera)
}) {
Icon(imageVector = SignalIcons.Camera.imageVector, contentDescription = stringResource(R.string.MediaSelectScreen__go_to_camera))
}
@@ -175,7 +175,7 @@ internal fun MediaSelectScreen(
.fillMaxSize()
.testTag(TestTags.MEDIA_SELECT_GRID)
.then(
if (state is MediaSelectScreenState.Files && !showPlaceholders) {
if (state is MediaSelectState.Files && !showPlaceholders) {
Modifier.dragToSelect(dragToSelectState)
} else {
Modifier
@@ -188,13 +188,13 @@ internal fun MediaSelectScreen(
}
} else {
when (state) {
is MediaSelectScreenState.Folders -> {
is MediaSelectState.Folders -> {
items(state.mediaFolders, key = { it.bucketId }) {
MediaFolderTile(it, onEvent)
}
}
is MediaSelectScreenState.Files -> {
is MediaSelectState.Files -> {
items(state.selectedMediaFolderItems, key = { it.uri }) { media ->
MediaTile(media = media, state.selectedMedia.indexOfFirst { it.uri == media.uri }, onEvent = onEvent)
}
@@ -239,7 +239,7 @@ internal fun MediaSelectScreen(
)
NextButton(state.selectedMedia.size) {
onEvent(MediaSelectScreenEvent.NavigateToEdit)
onEvent(MediaSelectScreenEvents.NavigateToEdit)
}
}
}
@@ -250,16 +250,16 @@ internal fun MediaSelectScreen(
/**
* Turns the covered index ranges reported by the gesture into batched selection events for the media those indices
* stand for. Only the file grid is homogeneous enough for this: one tile per item, so a grid index is an index into
* [MediaSelectScreenState.Files.selectedMediaFolderItems].
* [MediaSelectState.Files.selectedMediaFolderItems].
*/
@Composable
private fun rememberDragToSelectMediaState(
state: MediaSelectScreenState,
onEvent: (MediaSelectScreenEvent) -> Unit,
state: MediaSelectState,
onEvent: (MediaSelectScreenEvents) -> Unit,
gridState: LazyGridState
): DragToSelectState {
return rememberDragToSelectState(gridState) { event ->
val items = (state as? MediaSelectScreenState.Files)?.selectedMediaFolderItems ?: return@rememberDragToSelectState
val items = (state as? MediaSelectState.Files)?.selectedMediaFolderItems ?: return@rememberDragToSelectState
when (event) {
is DragSelectEvent.Started -> {
@@ -267,7 +267,7 @@ private fun rememberDragToSelectMediaState(
// and stops there, which is how the v2 gallery behaved.
val media = items.getOrNull(event.index)
if (media != null && state.selectedMedia.any { it.uri == media.uri }) {
onEvent(MediaSelectScreenEvent.MediaUnselected(setOf(media)))
onEvent(MediaSelectScreenEvents.MediaUnselected(setOf(media)))
cancel()
}
}
@@ -275,14 +275,14 @@ private fun rememberDragToSelectMediaState(
is DragSelectEvent.RangeSelected -> {
val media = event.indices.mapNotNullTo(mutableSetOf(), items::getOrNull)
if (media.isNotEmpty()) {
onEvent(MediaSelectScreenEvent.MediaSelected(media))
onEvent(MediaSelectScreenEvents.MediaSelected(media))
}
}
is DragSelectEvent.RangeUnselected -> {
val media = event.indices.mapNotNullTo(mutableSetOf(), items::getOrNull)
if (media.isNotEmpty()) {
onEvent(MediaSelectScreenEvent.MediaUnselected(media))
onEvent(MediaSelectScreenEvents.MediaUnselected(media))
}
}
}
@@ -371,7 +371,7 @@ private fun <T> WindowSizeClass.forWidthBreakpoint(
* selected-photos access has produced something to browse.
*/
@Composable
private fun LimitedAccessBar(onEvent: (MediaSelectScreenEvent) -> Unit) {
private fun LimitedAccessBar(onEvent: (MediaSelectScreenEvents) -> Unit) {
val menuController = remember { DropdownMenus.MenuController() }
Row(
@@ -406,7 +406,7 @@ private fun LimitedAccessBar(onEvent: (MediaSelectScreenEvent) -> Unit) {
@Composable
private fun MediaAccessCallToAction(
mediaPermissions: MediaPermissions,
onEvent: (MediaSelectScreenEvent) -> Unit,
onEvent: (MediaSelectScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val menuController = remember { DropdownMenus.MenuController() }
@@ -436,7 +436,7 @@ private fun MediaAccessCallToAction(
Box(modifier = Modifier.padding(top = 20.dp)) {
if (hasNoAccess) {
Buttons.LargeTonal(onClick = { onEvent(MediaSelectScreenEvent.RequestMediaPermissions) }) {
Buttons.LargeTonal(onClick = { onEvent(MediaSelectScreenEvents.RequestMediaPermissions) }) {
Text(text = stringResource(R.string.MediaSelectScreen__allow_access))
}
} else {
@@ -457,7 +457,7 @@ private fun MediaAccessCallToAction(
@Composable
private fun ManageAccessMenu(
menuController: DropdownMenus.MenuController,
onEvent: (MediaSelectScreenEvent) -> Unit
onEvent: (MediaSelectScreenEvents) -> Unit
) {
val context = LocalContext.current
@@ -466,7 +466,7 @@ private fun ManageAccessMenu(
menuController = controller,
drawableResId = R.drawable.symbol_album_tilt_24,
stringResId = R.string.MediaSelectScreen__select_more_photos,
onClick = { onEvent(MediaSelectScreenEvent.SelectMorePhotos) }
onClick = { onEvent(MediaSelectScreenEvents.SelectMorePhotos) }
)
DropdownMenus.ItemWithIcon(
@@ -495,13 +495,13 @@ private fun MediaTilePlaceholder() {
@Composable
private fun MediaFolderTile(
mediaFolder: MediaFolder,
onEvent: (MediaSelectScreenEvent) -> Unit
onEvent: (MediaSelectScreenEvents) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { onEvent(MediaSelectScreenEvent.FolderClick(mediaFolder)) },
onClick = { onEvent(MediaSelectScreenEvents.FolderClick(mediaFolder)) },
onClickLabel = mediaFolder.title,
role = Role.Button
),
@@ -541,7 +541,7 @@ private fun MediaFolderTile(
private fun MediaTile(
media: Media,
selectionIndex: Int,
onEvent: (MediaSelectScreenEvent) -> Unit
onEvent: (MediaSelectScreenEvents) -> Unit
) {
val scale by animateFloatAsState(
targetValue = if (selectionIndex >= 0) {
@@ -563,7 +563,7 @@ private fun MediaTile(
.aspectRatio(1f)
.background(color = MaterialTheme.colorScheme.surfaceVariant)
.clickable(
onClick = { onEvent(MediaSelectScreenEvent.MediaClick(media)) },
onClick = { onEvent(MediaSelectScreenEvents.MediaClick(media)) },
onClickLabel = media.fileName,
role = Role.Button
)
@@ -642,12 +642,12 @@ private fun NextButton(mediaSelectionCount: Int, onClick: () -> Unit) {
private fun SelectedMediaRow(
selectedMedia: List<Media>,
alignment: Alignment.Horizontal,
onEvent: (MediaSelectScreenEvent) -> Unit,
onEvent: (MediaSelectScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val listState = rememberLazyListState()
val reorderBuffer = rememberReorderBuffer(selectedMedia) { fromIndex, toIndex ->
onEvent(MediaSelectScreenEvent.ReorderSelectedMedia(fromIndex, toIndex))
onEvent(MediaSelectScreenEvents.ReorderSelectedMedia(fromIndex, toIndex))
}
val reorderableListState = rememberReorderableListState(
lazyListState = listState,
@@ -665,8 +665,8 @@ private fun SelectedMediaRow(
itemsIndexed(reorderBuffer.items, key = { _, media -> media.uri }) { index, media ->
ReorderableItem(reorderableListState, index) {
MediaThumbnail(media) {
onEvent(MediaSelectScreenEvent.SetFocusedMedia(media))
onEvent(MediaSelectScreenEvent.NavigateToEdit)
onEvent(MediaSelectScreenEvents.SetFocusedMedia(media))
onEvent(MediaSelectScreenEvents.NavigateToEdit)
}
}
}
@@ -703,7 +703,7 @@ private fun MediaThumbnail(
private fun MediaSelectScreenFolderPreview() {
Previews.Preview {
MediaSelectScreen(
state = MediaSelectScreenState.Folders(
state = MediaSelectState.Folders(
mediaFolders = rememberPreviewMediaFolders(20),
selectedMedia = emptyList()
),
@@ -721,13 +721,13 @@ private fun MediaSelectScreenMediaPreview() {
Previews.Preview {
MediaSelectScreen(
state = MediaSelectScreenState.Files(
state = MediaSelectState.Files(
selectedMediaFolder = folders.first(),
selectedMediaFolderItems = media,
selectedMedia = selectedMedia
),
onEvent = {
if (it is MediaSelectScreenEvent.MediaClick) {
if (it is MediaSelectScreenEvents.MediaClick) {
if (it.media in selectedMedia) {
selectedMedia.remove(it.media)
} else {
@@ -744,7 +744,7 @@ private fun MediaSelectScreenMediaPreview() {
private fun MediaSelectScreenNoPermissionPreview() {
Previews.Preview {
MediaSelectScreen(
state = MediaSelectScreenState.Folders(
state = MediaSelectState.Folders(
mediaFolders = emptyList(),
selectedMedia = emptyList(),
mediaPermissions = MediaPermissions.NONE
@@ -759,7 +759,7 @@ private fun MediaSelectScreenNoPermissionPreview() {
private fun MediaSelectScreenPartialPermissionEmptyPreview() {
Previews.Preview {
MediaSelectScreen(
state = MediaSelectScreenState.Folders(
state = MediaSelectState.Folders(
mediaFolders = emptyList(),
selectedMedia = emptyList(),
mediaPermissions = MediaPermissions.PARTIAL
@@ -774,7 +774,7 @@ private fun MediaSelectScreenPartialPermissionEmptyPreview() {
private fun MediaSelectScreenPartialPermissionPreview() {
Previews.Preview {
MediaSelectScreen(
state = MediaSelectScreenState.Folders(
state = MediaSelectState.Folders(
mediaFolders = rememberPreviewMediaFolders(4),
selectedMedia = emptyList(),
mediaPermissions = MediaPermissions.PARTIAL
@@ -819,7 +819,7 @@ private fun MediaTileSelectedPreview() {
media = rememberPreviewMedia(1).first(),
selectionIndex = if (isSelected) 0 else -1,
onEvent = {
if (it is MediaSelectScreenEvent.MediaClick) {
if (it is MediaSelectScreenEvents.MediaClick) {
isSelected = !isSelected
}
}
@@ -9,38 +9,38 @@ import org.signal.core.models.media.Media
import org.signal.core.models.media.MediaFolder
import org.signal.mediasend.MediaSendFlowState
sealed interface MediaSelectScreenEvent {
sealed interface MediaSelectScreenEvents {
/** The parent flow's state changed and needs to be merged into this screen's state. */
data class ParentStateChanged(val parentState: MediaSendFlowState) : MediaSelectScreenEvent {
data class ParentStateChanged(val parentState: MediaSendFlowState) : MediaSelectScreenEvents {
// The parent's state carries the message the user is typing and every item they have picked. Only the parts this
// screen reads are worth logging, and they are the only parts safe to.
override fun toString(): String = "ParentStateChanged(selectedMedia=${parentState.selectedMedia.size}, isSelectionRejected=${parentState.isSelectionRejected})"
}
/** The screen has stopped what the refusal was meant to stop, so the flow can drop it. */
data object SelectionRejectionShown : MediaSelectScreenEvent
data object SelectionRejectionShown : MediaSelectScreenEvents
data class FolderClick(val mediaFolder: MediaFolder?) : MediaSelectScreenEvent
data class MediaClick(val media: Media) : MediaSelectScreenEvent
data class FolderClick(val mediaFolder: MediaFolder?) : MediaSelectScreenEvents
data class MediaClick(val media: Media) : MediaSelectScreenEvents
/** A run of media covered by a drag across the grid. Batched, since a drag can cross many tiles in a single frame. */
data class MediaSelected(val media: Set<Media>) : MediaSelectScreenEvent
data class MediaSelected(val media: Set<Media>) : MediaSelectScreenEvents
/** A run of media that a drag has retracted back over, undoing its own selection. */
data class MediaUnselected(val media: Set<Media>) : MediaSelectScreenEvent
data class MediaUnselected(val media: Set<Media>) : MediaSelectScreenEvents
data class SetFocusedMedia(val media: Media) : MediaSelectScreenEvent
data class ReorderSelectedMedia(val fromIndex: Int, val toIndex: Int) : MediaSelectScreenEvent
data object NavigateToEdit : MediaSelectScreenEvent
data object NavigateToCamera : MediaSelectScreenEvent
data class SetFocusedMedia(val media: Media) : MediaSelectScreenEvents
data class ReorderSelectedMedia(val fromIndex: Int, val toIndex: Int) : MediaSelectScreenEvents
data object NavigateToEdit : MediaSelectScreenEvents
data object NavigateToCamera : MediaSelectScreenEvents
/** Re-read the gallery and the current permission level, e.g. after coming back from app settings. */
data object Refresh : MediaSelectScreenEvent
data object Refresh : MediaSelectScreenEvents
/** The up-front "Allow access" ask, made when we cannot read anything at all. */
data object RequestMediaPermissions : MediaSelectScreenEvent
data object RequestMediaPermissions : MediaSelectScreenEvents
/** Re-ask while holding selected-photos access, so the user can widen what we can see. */
data object SelectMorePhotos : MediaSelectScreenEvent
data object SelectMorePhotos : MediaSelectScreenEvents
}
@@ -8,7 +8,7 @@ package org.signal.mediasend.screens.select
import org.signal.core.models.media.Media
import org.signal.core.models.media.MediaFolder
sealed interface MediaSelectScreenState {
sealed interface MediaSelectState {
/** The flow's current selection, as last reported by the parent. */
val selectedMedia: List<Media>
@@ -27,7 +27,7 @@ sealed interface MediaSelectScreenState {
override val selectedMedia: List<Media>,
override val mediaPermissions: MediaPermissions = MediaPermissions.FULL,
override val isSelectionRejected: Boolean = false
) : MediaSelectScreenState {
) : MediaSelectState {
override val hasContent: Boolean
get() = mediaFolders.isNotEmpty()
}
@@ -38,17 +38,17 @@ sealed interface MediaSelectScreenState {
override val selectedMedia: List<Media>,
override val mediaPermissions: MediaPermissions = MediaPermissions.FULL,
override val isSelectionRejected: Boolean = false
) : MediaSelectScreenState {
) : MediaSelectState {
override val hasContent: Boolean
get() = selectedMediaFolderItems.isNotEmpty()
}
fun withParentState(selectedMedia: List<Media>, isSelectionRejected: Boolean): MediaSelectScreenState = when (this) {
fun withParentState(selectedMedia: List<Media>, isSelectionRejected: Boolean): MediaSelectState = when (this) {
is Folders -> copy(selectedMedia = selectedMedia, isSelectionRejected = isSelectionRejected)
is Files -> copy(selectedMedia = selectedMedia, isSelectionRejected = isSelectionRejected)
}
fun withMediaPermissions(mediaPermissions: MediaPermissions): MediaSelectScreenState = when (this) {
fun withMediaPermissions(mediaPermissions: MediaPermissions): MediaSelectState = when (this) {
is Folders -> copy(mediaPermissions = mediaPermissions)
is Files -> copy(mediaPermissions = mediaPermissions)
}
@@ -22,7 +22,7 @@ import org.signal.core.ui.compose.EventDrivenViewModel
import org.signal.core.ui.compose.Snackbars
import org.signal.core.util.logging.Log
import org.signal.mediasend.MediaSendDependencies
import org.signal.mediasend.MediaSendEvent
import org.signal.mediasend.MediaSendFlowEvent
import org.signal.mediasend.MediaSendFlowState
import org.signal.mediasend.MediaSendRepository
import org.signal.mediasend.R
@@ -32,39 +32,39 @@ import org.signal.mediasend.SnackbarEvent
* Drives the select screen, for one appearance of it: either the list of folders, or the contents of a single folder.
*
* The gallery and how much of it we are allowed to read are only of interest to this screen, so they live here. The
* selection is not -- the rest of the flow sends it -- so it arrives as [MediaSelectScreenEvent.ParentStateChanged] and
* selection is not -- the rest of the flow sends it -- so it arrives as [MediaSelectScreenEvents.ParentStateChanged] and
* is rendered from a copy in this screen's own state. Anything this screen wants done to it goes back out as a
* [MediaSendEvent].
* [MediaSendFlowEvent].
*
* @param mediaFolder The folder whose contents to show, or null for the list of folders.
*/
internal class MediaSelectViewModel(
private val parentState: StateFlow<MediaSendFlowState>,
private val parentEventEmitter: (MediaSendEvent) -> Unit,
private val parentEventEmitter: (MediaSendFlowEvent) -> Unit,
mediaFolder: MediaFolder?,
private val repository: MediaSendRepository = MediaSendDependencies.mediaSendRepository
) : EventDrivenViewModel<MediaSelectScreenEvent>(TAG) {
) : EventDrivenViewModel<MediaSelectScreenEvents>(TAG) {
companion object {
private val TAG = Log.tag(MediaSelectViewModel::class)
}
private val _state: MutableStateFlow<MediaSelectScreenState> = MutableStateFlow(
private val _state: MutableStateFlow<MediaSelectState> = MutableStateFlow(
if (mediaFolder != null) {
MediaSelectScreenState.Files(
MediaSelectState.Files(
selectedMediaFolder = mediaFolder,
selectedMediaFolderItems = emptyList(),
selectedMedia = emptyList()
)
} else {
MediaSelectScreenState.Folders(
MediaSelectState.Folders(
mediaFolders = emptyList(),
selectedMedia = emptyList()
)
}
)
val state: StateFlow<MediaSelectScreenState> = _state.asStateFlow()
val state: StateFlow<MediaSelectState> = _state.asStateFlow()
/** Hosted here, since this is the only screen that asks for the gallery's permissions. */
val readMediaPermission = MediaPermissionController()
@@ -72,35 +72,35 @@ internal class MediaSelectViewModel(
init {
parentState
.distinctUntilChangedBy { it.selectedMedia to it.isSelectionRejected }
.onEach { onEvent(MediaSelectScreenEvent.ParentStateChanged(it)) }
.onEach { onEvent(MediaSelectScreenEvents.ParentStateChanged(it)) }
.launchIn(viewModelScope)
refresh()
}
override suspend fun processEvent(event: MediaSelectScreenEvent) {
override suspend fun processEvent(event: MediaSelectScreenEvents) {
when (event) {
is MediaSelectScreenEvent.ParentStateChanged -> _state.update { it.withParentState(event.parentState.selectedMedia, event.parentState.isSelectionRejected) }
MediaSelectScreenEvent.SelectionRejectionShown -> parentEventEmitter(MediaSendEvent.SelectionRejectionShown)
is MediaSelectScreenEvent.FolderClick -> event.mediaFolder?.let { parentEventEmitter(MediaSendEvent.NavigateToFiles(it)) }
is MediaSelectScreenEvent.MediaClick -> applyMediaClickEvent(event.media)
is MediaSelectScreenEvent.MediaSelected -> parentEventEmitter(MediaSendEvent.AddMedia(event.media))
is MediaSelectScreenEvent.MediaUnselected -> parentEventEmitter(MediaSendEvent.RemoveMedia(event.media))
is MediaSelectScreenEvent.SetFocusedMedia -> parentEventEmitter(MediaSendEvent.SetFocusedMedia(event.media))
is MediaSelectScreenEvent.ReorderSelectedMedia -> parentEventEmitter(MediaSendEvent.ReorderSelectedMedia(event.fromIndex, event.toIndex))
MediaSelectScreenEvent.NavigateToEdit -> parentEventEmitter(MediaSendEvent.NavigateToEdit)
MediaSelectScreenEvent.NavigateToCamera -> parentEventEmitter(MediaSendEvent.NavigateToCamera)
MediaSelectScreenEvent.Refresh -> refresh()
MediaSelectScreenEvent.RequestMediaPermissions -> requestReadMediaPermissions(reportDenial = true)
MediaSelectScreenEvent.SelectMorePhotos -> requestReadMediaPermissions(reportDenial = false)
is MediaSelectScreenEvents.ParentStateChanged -> _state.update { it.withParentState(event.parentState.selectedMedia, event.parentState.isSelectionRejected) }
MediaSelectScreenEvents.SelectionRejectionShown -> parentEventEmitter(MediaSendFlowEvent.SelectionRejectionShown)
is MediaSelectScreenEvents.FolderClick -> event.mediaFolder?.let { parentEventEmitter(MediaSendFlowEvent.NavigateToFiles(it)) }
is MediaSelectScreenEvents.MediaClick -> applyMediaClickEvent(event.media)
is MediaSelectScreenEvents.MediaSelected -> parentEventEmitter(MediaSendFlowEvent.AddMedia(event.media))
is MediaSelectScreenEvents.MediaUnselected -> parentEventEmitter(MediaSendFlowEvent.RemoveMedia(event.media))
is MediaSelectScreenEvents.SetFocusedMedia -> parentEventEmitter(MediaSendFlowEvent.SetFocusedMedia(event.media))
is MediaSelectScreenEvents.ReorderSelectedMedia -> parentEventEmitter(MediaSendFlowEvent.ReorderSelectedMedia(event.fromIndex, event.toIndex))
MediaSelectScreenEvents.NavigateToEdit -> parentEventEmitter(MediaSendFlowEvent.NavigateToEdit)
MediaSelectScreenEvents.NavigateToCamera -> parentEventEmitter(MediaSendFlowEvent.NavigateToCamera)
MediaSelectScreenEvents.Refresh -> refresh()
MediaSelectScreenEvents.RequestMediaPermissions -> requestReadMediaPermissions(reportDenial = true)
MediaSelectScreenEvents.SelectMorePhotos -> requestReadMediaPermissions(reportDenial = false)
}
}
private fun applyMediaClickEvent(media: Media) {
if (_state.value.selectedMedia.any { it.uri == media.uri }) {
parentEventEmitter(MediaSendEvent.RemoveMedia(setOf(media)))
parentEventEmitter(MediaSendFlowEvent.RemoveMedia(setOf(media)))
} else {
parentEventEmitter(MediaSendEvent.AddMedia(setOf(media)))
parentEventEmitter(MediaSendFlowEvent.AddMedia(setOf(media)))
}
}
@@ -113,9 +113,9 @@ internal class MediaSelectViewModel(
viewModelScope.launch {
val mediaPermissions = MediaPermissions.current()
val reloaded: MediaSelectScreenState = when (val snapshot = _state.value) {
is MediaSelectScreenState.Folders -> snapshot.copy(mediaFolders = repository.getFolders())
is MediaSelectScreenState.Files -> snapshot.copy(selectedMediaFolderItems = repository.getMedia(snapshot.selectedMediaFolder.bucketId))
val reloaded: MediaSelectState = when (val snapshot = _state.value) {
is MediaSelectState.Folders -> snapshot.copy(mediaFolders = repository.getFolders())
is MediaSelectState.Files -> snapshot.copy(selectedMediaFolderItems = repository.getMedia(snapshot.selectedMediaFolder.bucketId))
}
// Only what the parent reports can have changed while we were reading, and that is not ours to overwrite.
@@ -137,7 +137,7 @@ internal class MediaSelectViewModel(
if (reportDenial && denied) {
parentEventEmitter(
MediaSendEvent.ShowSnackbar(
MediaSendFlowEvent.ShowSnackbar(
SnackbarEvent(
message = R.string.MediaSelectScreen__signal_needs_access_to_show_your_photos_and_videos,
duration = Snackbars.Duration.LONG
@@ -152,7 +152,7 @@ internal class MediaSelectViewModel(
class Factory(
private val parentState: StateFlow<MediaSendFlowState>,
private val parentEventEmitter: (MediaSendEvent) -> Unit,
private val parentEventEmitter: (MediaSendFlowEvent) -> Unit,
private val mediaFolder: MediaFolder?
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@@ -37,7 +37,7 @@ class AddAMessageRowTest {
@get:Rule
val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext())
private val events = mutableListOf<MediaEditScreenEvent>()
private val events = mutableListOf<MediaEditScreenEvents>()
private var nextClicks = 0
@Test
@@ -47,7 +47,7 @@ class AddAMessageRowTest {
composeTestRule.onNodeWithTag(TestTags.ADD_A_MESSAGE_NEXT_BUTTON).performClick()
assertEquals(1, nextClicks)
assertNull(events.filterIsInstance<MediaEditScreenEvent.ScheduleSendClick>().firstOrNull())
assertNull(events.filterIsInstance<MediaEditScreenEvents.ScheduleSendClick>().firstOrNull())
}
@Test
@@ -58,8 +58,8 @@ class AddAMessageRowTest {
composeTestRule.onNodeWithTag(TestTags.SCHEDULE_SEND_PICK_TIME_OPTION).performClick()
assertEquals(
MediaEditScreenEvent.ScheduleSendClick(ScheduleSendOption.PickTime),
events.filterIsInstance<MediaEditScreenEvent.ScheduleSendClick>().single()
MediaEditScreenEvents.ScheduleSendClick(ScheduleSendOption.PickTime),
events.filterIsInstance<MediaEditScreenEvents.ScheduleSendClick>().single()
)
assertEquals(0, nextClicks)
}
@@ -71,7 +71,7 @@ class AddAMessageRowTest {
composeTestRule.onNodeWithTag(TestTags.ADD_A_MESSAGE_NEXT_BUTTON).performTouchInput { longClick() }
composeTestRule.onNodeWithTag(TestTags.SCHEDULE_SEND_PICK_TIME_OPTION).assertDoesNotExist()
assertEquals(emptyList<MediaEditScreenEvent>(), events)
assertEquals(emptyList<MediaEditScreenEvents>(), events)
}
private fun setContent(canScheduleSend: Boolean) {
@@ -46,13 +46,13 @@ class MediaSelectScreenDragTest {
@get:Rule
val mediaSendDependenciesRule = MediaSendDependenciesRule(ApplicationProvider.getApplicationContext())
private val events = mutableListOf<MediaSelectScreenEvent>()
private val events = mutableListOf<MediaSelectScreenEvents>()
private val selected: Set<Media>
get() = events.filterIsInstance<MediaSelectScreenEvent.MediaSelected>().flatMap { it.media }.toSet()
get() = events.filterIsInstance<MediaSelectScreenEvents.MediaSelected>().flatMap { it.media }.toSet()
private val unselected: Set<Media>
get() = events.filterIsInstance<MediaSelectScreenEvent.MediaUnselected>().flatMap { it.media }.toSet()
get() = events.filterIsInstance<MediaSelectScreenEvents.MediaUnselected>().flatMap { it.media }.toSet()
@Test
fun `Given an unselected tile, when it is long pressed, then it alone is selected`() {
@@ -138,7 +138,7 @@ class MediaSelectScreenDragTest {
SignalTheme {
Box(modifier = Modifier.size(GRID_WIDTH.dp, GRID_HEIGHT.dp)) {
MediaSelectScreen(
state = MediaSelectScreenState.Files(
state = MediaSelectState.Files(
selectedMediaFolder = FOLDER,
selectedMediaFolderItems = MEDIA,
selectedMedia = selectedMedia