restore videos
This commit is contained in:
@@ -130,7 +130,11 @@ import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
|
||||
import {draftToComposerPosts, extractLocalRefs} from './drafts/state/api'
|
||||
import {
|
||||
draftToComposerPosts,
|
||||
extractLocalRefs,
|
||||
type RestoredVideo,
|
||||
} from './drafts/state/api'
|
||||
import {
|
||||
loadDraft,
|
||||
useCleanupPublishedDraftMutation,
|
||||
@@ -329,6 +333,92 @@ export const ComposePost = ({
|
||||
[composerDispatch],
|
||||
)
|
||||
|
||||
const restoreVideo = React.useCallback(
|
||||
async (postId: string, videoInfo: RestoredVideo) => {
|
||||
try {
|
||||
logger.debug('restoring video from draft', {
|
||||
postId,
|
||||
videoUri: videoInfo.uri,
|
||||
altText: videoInfo.altText,
|
||||
})
|
||||
|
||||
let asset: ImagePickerAsset
|
||||
|
||||
if (IS_WEB) {
|
||||
// Web: Convert blob URL to a File, then get video metadata (returns data URL)
|
||||
const response = await fetch(videoInfo.uri)
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], 'restored-video', {
|
||||
type: videoInfo.mimeType,
|
||||
})
|
||||
asset = await getVideoMetadata(file)
|
||||
} else {
|
||||
// Native: Use file URI directly with minimal asset properties
|
||||
// The native compressVideo only needs uri and mimeType
|
||||
asset = {
|
||||
uri: videoInfo.uri,
|
||||
mimeType: videoInfo.mimeType,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Start video processing using existing flow
|
||||
const abortController = new AbortController()
|
||||
composerDispatch({
|
||||
type: 'update_post',
|
||||
postId,
|
||||
postAction: {
|
||||
type: 'embed_add_video',
|
||||
asset,
|
||||
abortController,
|
||||
},
|
||||
})
|
||||
|
||||
// Restore alt text immediately
|
||||
if (videoInfo.altText) {
|
||||
composerDispatch({
|
||||
type: 'update_post',
|
||||
postId,
|
||||
postAction: {
|
||||
type: 'embed_update_video',
|
||||
videoAction: {
|
||||
type: 'update_alt_text',
|
||||
altText: videoInfo.altText,
|
||||
signal: abortController.signal,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Start video compression and upload
|
||||
processVideo(
|
||||
asset,
|
||||
videoAction => {
|
||||
composerDispatch({
|
||||
type: 'update_post',
|
||||
postId,
|
||||
postAction: {
|
||||
type: 'embed_update_video',
|
||||
videoAction,
|
||||
},
|
||||
})
|
||||
},
|
||||
agent,
|
||||
currentDid,
|
||||
abortController.signal,
|
||||
_,
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Failed to restore video from draft', {
|
||||
postId,
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
},
|
||||
[_, agent, currentDid, composerDispatch],
|
||||
)
|
||||
|
||||
const handleSelectDraft = React.useCallback(
|
||||
async (draftSummary: DraftSummary) => {
|
||||
logger.debug('loading draft for editing', {
|
||||
@@ -347,8 +437,11 @@ export const ComposePost = ({
|
||||
originalLocalRefCount: originalLocalRefs.size,
|
||||
})
|
||||
|
||||
// Convert server draft to composer posts
|
||||
const posts = draftToComposerPosts(draftSummary.draft, loadedMedia)
|
||||
// Convert server draft to composer posts (videos returned separately)
|
||||
const {posts, restoredVideos} = draftToComposerPosts(
|
||||
draftSummary.draft,
|
||||
loadedMedia,
|
||||
)
|
||||
|
||||
// Dispatch restore action (this also sets draftId in state)
|
||||
composerDispatch({
|
||||
@@ -360,8 +453,15 @@ export const ComposePost = ({
|
||||
loadedMedia,
|
||||
originalLocalRefs,
|
||||
})
|
||||
|
||||
// Initiate video processing for any restored videos
|
||||
// This is async but we don't await - videos process in the background
|
||||
for (const [postIndex, videoInfo] of restoredVideos) {
|
||||
const postId = posts[postIndex].id
|
||||
restoreVideo(postId, videoInfo)
|
||||
}
|
||||
},
|
||||
[composerDispatch],
|
||||
[composerDispatch, restoreVideo],
|
||||
)
|
||||
|
||||
const [publishOnUpload, setPublishOnUpload] = useState(false)
|
||||
|
||||
@@ -17,6 +17,31 @@ import {type DraftPostDisplay, type DraftSummary} from './schema'
|
||||
|
||||
const TENOR_HOSTNAME = 'media.tenor.com'
|
||||
|
||||
/**
|
||||
* Video data from a draft that needs to be restored by re-processing.
|
||||
* Contains the local file URI, alt text, and mime type to restore.
|
||||
*/
|
||||
export type RestoredVideo = {
|
||||
uri: string
|
||||
altText: string
|
||||
mimeType: string
|
||||
localRefPath: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse mime type from video localRefPath.
|
||||
* Format: `video:${mimeType}:${nanoid()}` (new) or `video:${nanoid()}` (legacy)
|
||||
*/
|
||||
function parseVideoMimeType(localRefPath: string): string {
|
||||
const parts = localRefPath.split(':')
|
||||
// New format: video:video/mp4:abc123 -> parts[1] is mime type
|
||||
// Legacy format: video:abc123 -> no mime type, default to video/mp4
|
||||
if (parts.length >= 3 && parts[1].includes('/')) {
|
||||
return parts[1]
|
||||
}
|
||||
return 'video/mp4' // Default for legacy drafts
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ComposerState to server Draft format for saving.
|
||||
* Returns both the draft and a map of localRef paths to their source paths.
|
||||
@@ -165,6 +190,7 @@ function serializeImages(
|
||||
|
||||
/**
|
||||
* Serialize video to server format with localRef path.
|
||||
* The localRef path encodes the mime type: `video:${mimeType}:${nanoid()}`
|
||||
*/
|
||||
function serializeVideo(
|
||||
videoState: VideoState,
|
||||
@@ -175,7 +201,9 @@ function serializeVideo(
|
||||
return undefined
|
||||
}
|
||||
|
||||
const localRefPath = `video:${nanoid()}`
|
||||
// Encode mime type in the path for restoration
|
||||
const mimeType = videoState.video.mimeType || 'video/mp4'
|
||||
const localRefPath = `video:${mimeType}:${nanoid()}`
|
||||
localRefPaths.set(localRefPath, videoState.video.uri)
|
||||
|
||||
return {
|
||||
@@ -345,13 +373,19 @@ function parseGifFromUrl(
|
||||
|
||||
/**
|
||||
* Convert server Draft back to composer-compatible format for restoration.
|
||||
* Returns partial state that can be merged with initial composer state.
|
||||
* Returns posts and a map of videos that need to be restored by re-processing.
|
||||
*
|
||||
* Videos cannot be restored synchronously like images because they need to go through
|
||||
* the compression and upload pipeline. The caller should handle the restoredVideos
|
||||
* by initiating video processing for each entry.
|
||||
*/
|
||||
export function draftToComposerPosts(
|
||||
draft: AppBskyDraftDefs.Draft,
|
||||
loadedMedia: Map<string, string>,
|
||||
): PostDraft[] {
|
||||
return draft.posts.map((post, index) => {
|
||||
): {posts: PostDraft[]; restoredVideos: Map<number, RestoredVideo>} {
|
||||
const restoredVideos = new Map<number, RestoredVideo>()
|
||||
|
||||
const posts = draft.posts.map((post, index) => {
|
||||
const richtext = new RichText({text: post.text || ''})
|
||||
richtext.detectFacetsWithoutResolution()
|
||||
|
||||
@@ -428,6 +462,27 @@ export function draftToComposerPosts(
|
||||
}
|
||||
}
|
||||
|
||||
// Collect video for restoration (processed async by caller)
|
||||
if (post.embedVideos && post.embedVideos.length > 0) {
|
||||
const vid = post.embedVideos[0]
|
||||
const videoUri = loadedMedia.get(vid.localRef.path)
|
||||
if (videoUri) {
|
||||
const mimeType = parseVideoMimeType(vid.localRef.path)
|
||||
logger.debug('found video to restore', {
|
||||
localRefPath: vid.localRef.path,
|
||||
videoUri,
|
||||
altText: vid.alt,
|
||||
mimeType,
|
||||
})
|
||||
restoredVideos.set(index, {
|
||||
uri: videoUri,
|
||||
altText: vid.alt || '',
|
||||
mimeType,
|
||||
localRefPath: vid.localRef.path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Restore quote embed
|
||||
if (post.embedRecords && post.embedRecords.length > 0) {
|
||||
const record = post.embedRecords[0]
|
||||
@@ -461,6 +516,8 @@ export function draftToComposerPosts(
|
||||
embed,
|
||||
} as PostDraft
|
||||
})
|
||||
|
||||
return {posts, restoredVideos}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user