Fix story pre-upload behavior for v3 media.

This commit is contained in:
Alex Hart
2026-08-03 16:51:09 -03:00
parent 2bf94f2d1c
commit 91ffba810f
4 changed files with 50 additions and 18 deletions
@@ -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.
*/
@@ -253,12 +253,8 @@ object MediaSendV3Repository : MediaSendRepository {
return MediaConstraints.isVideoTranscodeAvailable()
}
override suspend fun getStorySendRequirements(media: List<Media>): 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<Media>): Map<Uri, StorySendRequirements> = withContext(Dispatchers.IO) {
media.associate { it.uri to Stories.MediaTransform.getSendRequirements(it).toFeatureSendRequirements() }
}
override suspend fun checkUntrustedIdentities(contactIds: Set<Long>, since: Long): List<Long> = withContext(Dispatchers.Default) {
@@ -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<Media>): StorySendRequirements
suspend fun getStorySendRequirements(media: List<Media>): Map<Uri, StorySendRequirements>
/**
* 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<StorySendRequirements>.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
}
}
}
@@ -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<Media>) {
/**
* @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<Media>, storySendRequirements: Map<Uri, StorySendRequirements>) {
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<Media>) {
private suspend fun updateStorySendRequirements(media: List<Media>): Map<Uri, StorySendRequirements> {
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