feat(drafts): skip re-compression when restoring pre-compressed videos

Update restoreVideo() to:
- Use restore_video_to_post action to avoid marking composer dirty
- Check compressed flag from stored video metadata
- If video is pre-compressed, skip compression and go straight to upload
- Pass localRefPath through to preserve video identity

This prevents unnecessary re-compression when restoring drafts with
videos that were already compressed before saving.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-30 20:49:00 +02:00
parent fb459ad61f
commit fd769e32d4
+173 -53
View File
@@ -69,6 +69,7 @@ import {
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type CompressedVideo} from '#/lib/media/video/types'
import {mimeToExt} from '#/lib/media/video/util' import {mimeToExt} from '#/lib/media/video/util'
import {useCallOnce} from '#/lib/once' import {useCallOnce} from '#/lib/once'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
@@ -164,6 +165,7 @@ import {
NO_VIDEO, NO_VIDEO,
type NoVideoState, type NoVideoState,
processVideo, processVideo,
uploadPrecompressedVideo,
type VideoState, type VideoState,
} from './state/video' } from './state/video'
import {type TextInputRef} from './text-input/TextInput.types' import {type TextInputRef} from './text-input/TextInput.types'
@@ -362,10 +364,14 @@ export const ComposePost = ({
postId, postId,
videoUri: videoInfo.uri, videoUri: videoInfo.uri,
altText: videoInfo.altText, altText: videoInfo.altText,
compressed: videoInfo.compressed,
captionCount: videoInfo.captions.length, captionCount: videoInfo.captions.length,
}) })
let asset: ImagePickerAsset let asset: ImagePickerAsset
let videoUri = videoInfo.uri
let videoSize = 0
let videoBytes: ArrayBuffer | undefined
if (IS_WEB) { if (IS_WEB) {
// Web: Convert blob URL to a File, then get video metadata (returns data URL) // Web: Convert blob URL to a File, then get video metadata (returns data URL)
@@ -375,8 +381,11 @@ export const ComposePost = ({
type: videoInfo.mimeType, type: videoInfo.mimeType,
}) })
asset = await getVideoMetadata(file) asset = await getVideoMetadata(file)
// For web, the data URL is used directly
videoUri = asset.uri
videoSize = blob.size
videoBytes = await blob.arrayBuffer()
} else { } else {
let uri = videoInfo.uri
if (IS_ANDROID) { if (IS_ANDROID) {
// Android: expo-file-system double-encodes filenames with special chars. // Android: expo-file-system double-encodes filenames with special chars.
// The file exists, but react-native-compressor's MediaMetadataRetriever // The file exists, but react-native-compressor's MediaMetadataRetriever
@@ -392,79 +401,190 @@ export const ComposePost = ({
source: videoInfo.uri, source: videoInfo.uri,
temp: tempFile.uri, temp: tempFile.uri,
}) })
uri = tempFile.uri videoUri = tempFile.uri
videoSize = tempFile.size ?? 0
} else {
// iOS
const sourceFile = new FileSystem.File(videoInfo.uri)
videoSize = sourceFile.size ?? 0
} }
asset = await getVideoMetadata(uri) asset = await getVideoMetadata(videoUri)
} }
// Start video processing using existing flow
const abortController = new AbortController() const abortController = new AbortController()
composerDispatch({
type: 'update_post',
postId,
postAction: {
type: 'embed_add_video',
asset,
abortController,
},
})
// Restore alt text immediately if (videoInfo.compressed) {
if (videoInfo.altText) { // Video is already compressed - skip compression step
logger.debug('restoreVideo: using pre-compressed video', {
postId,
localRefPath: videoInfo.localRefPath,
})
// Create CompressedVideo object from stored data
const compressedVideo: CompressedVideo = {
uri: videoUri,
mimeType: videoInfo.mimeType,
size: videoSize,
bytes: videoBytes,
}
// Add video to post (at compressing status initially)
composerDispatch({ composerDispatch({
type: 'update_post', type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_add_video',
asset,
abortController,
localRefPath: videoInfo.localRefPath,
},
})
// Manually update to uploading state with the compressed video
composerDispatch({
type: 'restore_video_to_post',
postId, postId,
postAction: { postAction: {
type: 'embed_update_video', type: 'embed_update_video',
videoAction: { videoAction: {
type: 'update_alt_text', type: 'compressing_to_uploading',
altText: videoInfo.altText, video: compressedVideo,
signal: abortController.signal, signal: abortController.signal,
}, },
}, },
}) })
}
// Restore captions (web only - captions use File objects) // Restore alt text
if (IS_WEB && videoInfo.captions.length > 0) { if (videoInfo.altText) {
const captionTracks = videoInfo.captions.map(c => ({
lang: c.lang,
file: new File([c.content], `caption-${c.lang}.vtt`, {
type: 'text/vtt',
}),
}))
composerDispatch({
type: 'update_post',
postId,
postAction: {
type: 'embed_update_video',
videoAction: {
type: 'update_captions',
updater: () => captionTracks,
signal: abortController.signal,
},
},
})
}
// Start video compression and upload
processVideo(
asset,
videoAction => {
composerDispatch({ composerDispatch({
type: 'update_post', type: 'restore_video_to_post',
postId, postId,
postAction: { postAction: {
type: 'embed_update_video', type: 'embed_update_video',
videoAction, videoAction: {
type: 'update_alt_text',
altText: videoInfo.altText,
signal: abortController.signal,
},
}, },
}) })
}, }
agent,
currentDid, // Restore captions (web only)
abortController.signal, if (IS_WEB && videoInfo.captions.length > 0) {
_, const captionTracks = videoInfo.captions.map(c => ({
) lang: c.lang,
file: new File([c.content], `caption-${c.lang}.vtt`, {
type: 'text/vtt',
}),
}))
composerDispatch({
type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_update_video',
videoAction: {
type: 'update_captions',
updater: () => captionTracks,
signal: abortController.signal,
},
},
})
}
// Start upload (skip compression)
uploadPrecompressedVideo(
compressedVideo,
videoAction => {
composerDispatch({
type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_update_video',
videoAction,
},
})
},
agent,
currentDid,
abortController.signal,
_,
)
} else {
// Video not compressed yet - start from beginning
logger.debug('restoreVideo: starting full compression', {
postId,
localRefPath: videoInfo.localRefPath,
})
composerDispatch({
type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_add_video',
asset,
abortController,
localRefPath: videoInfo.localRefPath,
},
})
// Restore alt text immediately
if (videoInfo.altText) {
composerDispatch({
type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_update_video',
videoAction: {
type: 'update_alt_text',
altText: videoInfo.altText,
signal: abortController.signal,
},
},
})
}
// Restore captions (web only - captions use File objects)
if (IS_WEB && videoInfo.captions.length > 0) {
const captionTracks = videoInfo.captions.map(c => ({
lang: c.lang,
file: new File([c.content], `caption-${c.lang}.vtt`, {
type: 'text/vtt',
}),
}))
composerDispatch({
type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_update_video',
videoAction: {
type: 'update_captions',
updater: () => captionTracks,
signal: abortController.signal,
},
},
})
}
// Start video compression and upload
processVideo(
asset,
videoAction => {
composerDispatch({
type: 'restore_video_to_post',
postId,
postAction: {
type: 'embed_update_video',
videoAction,
},
})
},
agent,
currentDid,
abortController.signal,
_,
)
}
} catch (e) { } catch (e) {
logger.error('Failed to restore video from draft', { logger.error('Failed to restore video from draft', {
postId, postId,