diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Extensions.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Extensions.kt index 2f8843ee99..6730965449 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Extensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Extensions.kt @@ -25,6 +25,15 @@ fun StorySendRequirements.toAppSendRequirements(): Stories.MediaTransform.SendRe StorySendRequirements.REQUIRES_CROP -> Stories.MediaTransform.SendRequirements.REQUIRES_CLIP } +/** + * Maps the app-layer [Stories.MediaTransform.SendRequirements] to the feature-module [StorySendRequirements]. + */ +fun Stories.MediaTransform.SendRequirements.toFeatureSendRequirements(): StorySendRequirements = when (this) { + Stories.MediaTransform.SendRequirements.VALID_DURATION -> StorySendRequirements.CAN_SEND + Stories.MediaTransform.SendRequirements.CAN_NOT_SEND -> StorySendRequirements.CAN_NOT_SEND + Stories.MediaTransform.SendRequirements.REQUIRES_CLIP -> StorySendRequirements.REQUIRES_CROP +} + /** * Turns a sticker pick into a [Renderer] the image editor can place. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt index 55b536e4f4..0e02a2bae1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt @@ -253,12 +253,8 @@ object MediaSendV3Repository : MediaSendRepository { return MediaConstraints.isVideoTranscodeAvailable() } - override suspend fun getStorySendRequirements(media: List): StorySendRequirements = withContext(Dispatchers.IO) { - when (Stories.MediaTransform.getSendRequirements(media)) { - Stories.MediaTransform.SendRequirements.VALID_DURATION -> StorySendRequirements.CAN_SEND - Stories.MediaTransform.SendRequirements.REQUIRES_CLIP -> StorySendRequirements.REQUIRES_CROP - Stories.MediaTransform.SendRequirements.CAN_NOT_SEND -> StorySendRequirements.CAN_NOT_SEND - } + override suspend fun getStorySendRequirements(media: List): Map = withContext(Dispatchers.IO) { + media.associate { it.uri to Stories.MediaTransform.getSendRequirements(it).toFeatureSendRequirements() } } override suspend fun checkUntrustedIdentities(contactIds: Set, since: Long): List = withContext(Dispatchers.Default) { diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt index c161518495..62b301675a 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendRepository.kt @@ -104,9 +104,12 @@ interface MediaSendRepository { fun isVideoTranscodeAvailable(): Boolean /** - * Gets story send requirements for the given media. + * Gets the story send requirement for each of the given media, keyed by URI as the selection's stable identity. + * + * Per-item because pre-upload eligibility is decided per media, while the UI needs the whole selection's + * requirement. Deriving the latter from this map keeps duration probing to a single pass. */ - suspend fun getStorySendRequirements(media: List): StorySendRequirements + suspend fun getStorySendRequirements(media: List): Map /** * Checks for untrusted identity records among the given contacts. @@ -237,3 +240,17 @@ enum class StorySendRequirements { /** Requires cropping before sending to stories. */ REQUIRES_CROP } + +/** + * The strictest requirement across [this], which is how a send treats a selection: one item that cannot be sent + * blocks the selection, and one item needing a crop makes the selection need one. Empty selections can send. + */ +fun Collection.strictest(): StorySendRequirements { + return fold(StorySendRequirements.CAN_SEND) { left, right -> + when { + left == StorySendRequirements.CAN_NOT_SEND || right == StorySendRequirements.CAN_NOT_SEND -> StorySendRequirements.CAN_NOT_SEND + left == StorySendRequirements.REQUIRES_CROP || right == StorySendRequirements.REQUIRES_CROP -> StorySendRequirements.REQUIRES_CROP + else -> StorySendRequirements.CAN_SEND + } + } +} diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt index f5646bda42..933eb08a34 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt @@ -162,8 +162,6 @@ class MediaSendViewModel( .distinctUntilChanged() init { - // Matches legacy behavior: VM subscribes to connectivity updates and derives - // isPreUploadEnabled from metered state. viewModelScope.launch { isMeteredFlow.collect { metered -> updateState { copy(isMeteredConnection = metered, isPreUploadEnabled = shouldPreUpload(metered)) } @@ -607,11 +605,11 @@ class MediaSendViewModel( } // Update story requirements - updateStorySendRequirements(updatedMedia) + val storySendRequirements = updateStorySendRequirements(updatedMedia) // Start pre-uploads for new media val newMedia = updatedMedia.filter { item -> media.any { it.uri == item.uri } } - startUpload(newMedia) + startUpload(newMedia, storySendRequirements) } filterResult.error?.let { onMediaFilterError(it, isSelectionEmpty = filterResult.filteredMedia.isEmpty()) } @@ -787,14 +785,20 @@ class MediaSendViewModel( //region Pre-Upload Management - private fun startUpload(media: List) { + /** + * @param storySendRequirements Requirements for the current selection, so a story destination can skip media that + * will be clipped at send time and uploaded as a different asset. + */ + private fun startUpload(media: List, storySendRequirements: Map) { val snapshot = state.value if (!snapshot.isPreUploadEnabled) return - val filteredPreUploadMedia = if (snapshot.mode is MediaSendActivityContract.Mode.SingleRecipient) { + val isChatDestination = (snapshot.mode is MediaSendActivityContract.Mode.SingleRecipient && !snapshot.isStory) || !snapshot.storiesEnabled + + val filteredPreUploadMedia = if (isChatDestination) { media.filter { !ContentTypeUtil.isDocumentType(it.contentType) } } else { - media.filter { ContentTypeUtil.isStorySupportedType(it.contentType) } + media.filter { storySendRequirements[it.uri] != StorySendRequirements.REQUIRES_CROP } } preUploadController.startUpload(filteredPreUploadMedia, snapshot.recipientId) @@ -1100,9 +1104,10 @@ class MediaSendViewModel( * Computed for every flow, not just story flows: the contact picker consults this before allowing a story * to be selected, so leaving it at its default would strip story selections made mid-flow. */ - private suspend fun updateStorySendRequirements(media: List) { + private suspend fun updateStorySendRequirements(media: List): Map { val requirements = repository.getStorySendRequirements(media) - updateState { copy(storySendRequirements = requirements) } + updateState { copy(storySendRequirements = requirements.values.strictest()) } + return requirements } //endregion @@ -1307,7 +1312,12 @@ class MediaSendViewModel( preUploadController.deleteAbandonedAttachments() } - private fun shouldPreUpload(metered: Boolean): Boolean = !metered + /** + * A flow that picks its destination mid-flight has nothing to attribute an upload to yet, so it waits for the send. + */ + private fun shouldPreUpload(metered: Boolean): Boolean { + return !metered && args.mode != MediaSendActivityContract.Mode.ChooseAfterMediaSelection + } //endregion