diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index cd5c6f1a83..fe79b981d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt index 00220a5a3e..e4ffd7eb85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 60ca6d9595..9157d5236e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt index a9d39083f3..14ea8c6824 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt @@ -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() } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendEventHandler.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendEventHandler.kt new file mode 100644 index 0000000000..5512ed7e85 --- /dev/null +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendEventHandler.kt @@ -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) +} diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowEvent.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowEvent.kt index 262458fdb0..791dde91e0 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowEvent.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowEvent.kt @@ -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) : MediaSendEvent - data class RemoveMedia(val media: Set) : 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) : MediaSendFlowEvent + data class RemoveMedia(val media: Set) : 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) : HudCommand + data class NavigateToFiles(val mediaFolder: MediaFolder) : MediaSendFlowEvent + data object NavigateToEdit : MediaSendFlowEvent + data object NavigateToCamera : MediaSendFlowEvent } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowHudCommand.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowHudCommand.kt new file mode 100644 index 0000000000..8e50b86101 --- /dev/null +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowHudCommand.kt @@ -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) : MediaSendFlowHudCommand +} diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt index a647e949fa..4f92813232 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendFlowViewModel.kt @@ -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(Channel.BUFFERED) - val hudCommands: Flow = hudCommandChannel.receiveAsFlow() + private val hudCommandChannel = Channel(Channel.BUFFERED) + val hudCommands: Flow = 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) } } } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavigation.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavigation.kt index 9a863a0717..62d90a1230 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavigation.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavigation.kt @@ -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) } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavKey.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRoute.kt similarity index 69% rename from feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavKey.kt rename to feature/media-send/src/main/java/org/signal/mediasend/MediaSendRoute.kt index cb083fa806..63fba091f1 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendNavKey.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRoute.kt @@ -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 } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt index 48dfa9a279..0309543c81 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendScreen.kt @@ -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(factory = MediaSendFlowViewModel.Factory(args = contractArgs)) diff --git a/feature/media-send/src/main/java/org/signal/mediasend/NavBackStackExtensions.kt b/feature/media-send/src/main/java/org/signal/mediasend/NavBackStackExtensions.kt index 1fbcedb7aa..2e72f27832 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/NavBackStackExtensions.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/NavBackStackExtensions.kt @@ -10,27 +10,27 @@ import androidx.navigation3.runtime.NavKey import org.signal.core.models.media.MediaFolder internal fun NavBackStack.goToEdit() { - goToSingle(MediaSendNavKey.Edit) + goToSingle(MediaSendRoute.Edit) } internal fun NavBackStack.goToSend() { - goToSingle(MediaSendNavKey.Send) + goToSingle(MediaSendRoute.Send) } internal fun NavBackStack.goToFolders() { - goToSingle(MediaSendNavKey.Select.Folders) + goToSingle(MediaSendRoute.Select.Folders) } internal fun NavBackStack.goToFiles(mediaFolder: MediaFolder) { - add(MediaSendNavKey.Select.Files(mediaFolder)) + add(MediaSendRoute.Select.Files(mediaFolder)) } internal fun NavBackStack.goToTextStory() { - goToSingle(MediaSendNavKey.Capture.TextStory) + goToSingle(MediaSendRoute.Capture.TextStory) } internal fun NavBackStack.goToCamera() { - goToSingle(MediaSendNavKey.Capture.Camera) + goToSingle(MediaSendRoute.Capture.Camera) } /** diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXFragment.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXFragment.kt index 6210d82d2f..6acadd628b 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXFragment.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXFragment.kt @@ -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) } } } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXScreenEvent.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXScreenEvents.kt similarity index 61% rename from feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXScreenEvent.kt rename to feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXScreenEvents.kt index c0acc759c6..e216ef7fcd 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXScreenEvent.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/CameraXScreenEvents.kt @@ -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 } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCameraCaptureScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCameraCaptureScreen.kt index f61c319fff..1af9e4a442 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCameraCaptureScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCameraCaptureScreen.kt @@ -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 diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreen.kt index 145144a119..16492dfc01 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreen.kt @@ -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, - 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, - 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 } } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreenEvent.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreenEvent.kt deleted file mode 100644 index 39919b5314..0000000000 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreenEvent.kt +++ /dev/null @@ -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 -} diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreenEvents.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreenEvents.kt new file mode 100644 index 0000000000..ce4b47bdfb --- /dev/null +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/MediaCaptureScreenEvents.kt @@ -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 +} diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/TextStoryBar.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/TextStoryBar.kt index 822c61e359..892aa96d3a 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/TextStoryBar.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/capture/TextStoryBar.kt @@ -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( diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/AddAMessageRow.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/AddAMessageRow.kt index 75bba4e393..a6cc979c75 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/AddAMessageRow.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/AddAMessageRow.kt @@ -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)) } ) } } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreen.kt index 9060b7bb39..2df063f13f 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreen.kt @@ -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, diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreenEvent.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreenEvents.kt similarity index 61% rename from feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreenEvent.kt rename to feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreenEvents.kt index 691567989e..6b31124481 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreenEvent.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditScreenEvents.kt @@ -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 } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditorToolbar.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditorToolbar.kt index 7efdfa8b84..5af2ff283c 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditorToolbar.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/MediaEditorToolbar.kt @@ -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) ) } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/image/ImageEditorToolbar.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/image/ImageEditorToolbar.kt index ddaa8bcc0d..6c35d7e785 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/image/ImageEditorToolbar.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/image/ImageEditorToolbar.kt @@ -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) } ) diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/video/VideoTrimBar.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/video/VideoTrimBar.kt index 657f8f293a..d11c01b6e5 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/video/VideoTrimBar.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/edit/video/VideoTrimBar.kt @@ -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, diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreen.kt index 44aec85c89..7afb5d99f3 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreen.kt @@ -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 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, 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 } } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenEvent.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenEvents.kt similarity index 77% rename from feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenEvent.kt rename to feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenEvents.kt index 69be91adfc..5ad4ec116c 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenEvent.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenEvents.kt @@ -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) : MediaSelectScreenEvent + data class MediaSelected(val media: Set) : MediaSelectScreenEvents /** A run of media that a drag has retracted back over, undoing its own selection. */ - data class MediaUnselected(val media: Set) : MediaSelectScreenEvent + data class MediaUnselected(val media: Set) : 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 } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenState.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectState.kt similarity index 90% rename from feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenState.kt rename to feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectState.kt index b3d598633c..622ecedcc3 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectScreenState.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectState.kt @@ -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 @@ -27,7 +27,7 @@ sealed interface MediaSelectScreenState { override val selectedMedia: List, 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, override val mediaPermissions: MediaPermissions = MediaPermissions.FULL, override val isSelectionRejected: Boolean = false - ) : MediaSelectScreenState { + ) : MediaSelectState { override val hasContent: Boolean get() = selectedMediaFolderItems.isNotEmpty() } - fun withParentState(selectedMedia: List, isSelectionRejected: Boolean): MediaSelectScreenState = when (this) { + fun withParentState(selectedMedia: List, 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) } diff --git a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectViewModel.kt b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectViewModel.kt index ef4a16b8d6..8110acb6fa 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectViewModel.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/screens/select/MediaSelectViewModel.kt @@ -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, - private val parentEventEmitter: (MediaSendEvent) -> Unit, + private val parentEventEmitter: (MediaSendFlowEvent) -> Unit, mediaFolder: MediaFolder?, private val repository: MediaSendRepository = MediaSendDependencies.mediaSendRepository -) : EventDrivenViewModel(TAG) { +) : EventDrivenViewModel(TAG) { companion object { private val TAG = Log.tag(MediaSelectViewModel::class) } - private val _state: MutableStateFlow = MutableStateFlow( + private val _state: MutableStateFlow = 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 = _state.asStateFlow() + val state: StateFlow = _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, - private val parentEventEmitter: (MediaSendEvent) -> Unit, + private val parentEventEmitter: (MediaSendFlowEvent) -> Unit, private val mediaFolder: MediaFolder? ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") diff --git a/feature/media-send/src/test/java/org/signal/mediasend/screens/edit/AddAMessageRowTest.kt b/feature/media-send/src/test/java/org/signal/mediasend/screens/edit/AddAMessageRowTest.kt index c77c6c56d4..f663084760 100644 --- a/feature/media-send/src/test/java/org/signal/mediasend/screens/edit/AddAMessageRowTest.kt +++ b/feature/media-send/src/test/java/org/signal/mediasend/screens/edit/AddAMessageRowTest.kt @@ -37,7 +37,7 @@ class AddAMessageRowTest { @get:Rule val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) - private val events = mutableListOf() + private val events = mutableListOf() 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().firstOrNull()) + assertNull(events.filterIsInstance().firstOrNull()) } @Test @@ -58,8 +58,8 @@ class AddAMessageRowTest { composeTestRule.onNodeWithTag(TestTags.SCHEDULE_SEND_PICK_TIME_OPTION).performClick() assertEquals( - MediaEditScreenEvent.ScheduleSendClick(ScheduleSendOption.PickTime), - events.filterIsInstance().single() + MediaEditScreenEvents.ScheduleSendClick(ScheduleSendOption.PickTime), + events.filterIsInstance().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(), events) + assertEquals(emptyList(), events) } private fun setContent(canScheduleSend: Boolean) { diff --git a/feature/media-send/src/test/java/org/signal/mediasend/screens/select/MediaSelectScreenDragTest.kt b/feature/media-send/src/test/java/org/signal/mediasend/screens/select/MediaSelectScreenDragTest.kt index 9f9a9e2864..bf1cdc7df5 100644 --- a/feature/media-send/src/test/java/org/signal/mediasend/screens/select/MediaSelectScreenDragTest.kt +++ b/feature/media-send/src/test/java/org/signal/mediasend/screens/select/MediaSelectScreenDragTest.kt @@ -46,13 +46,13 @@ class MediaSelectScreenDragTest { @get:Rule val mediaSendDependenciesRule = MediaSendDependenciesRule(ApplicationProvider.getApplicationContext()) - private val events = mutableListOf() + private val events = mutableListOf() private val selected: Set - get() = events.filterIsInstance().flatMap { it.media }.toSet() + get() = events.filterIsInstance().flatMap { it.media }.toSet() private val unselected: Set - get() = events.filterIsInstance().flatMap { it.media }.toSet() + get() = events.filterIsInstance().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