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