save/restore captions
This commit is contained in:
@@ -340,6 +340,7 @@ export const ComposePost = ({
|
||||
postId,
|
||||
videoUri: videoInfo.uri,
|
||||
altText: videoInfo.altText,
|
||||
captionCount: videoInfo.captions.length,
|
||||
})
|
||||
|
||||
let asset: ImagePickerAsset
|
||||
@@ -391,6 +392,28 @@ export const ComposePost = ({
|
||||
})
|
||||
}
|
||||
|
||||
// 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: 'update_post',
|
||||
postId,
|
||||
postAction: {
|
||||
type: 'embed_update_video',
|
||||
videoAction: {
|
||||
type: 'update_captions',
|
||||
updater: () => captionTracks,
|
||||
signal: abortController.signal,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Start video compression and upload
|
||||
processVideo(
|
||||
asset,
|
||||
|
||||
@@ -19,13 +19,14 @@ 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.
|
||||
* Contains the local file URI, alt text, mime type, and captions to restore.
|
||||
*/
|
||||
export type RestoredVideo = {
|
||||
uri: string
|
||||
altText: string
|
||||
mimeType: string
|
||||
localRefPath: string
|
||||
captions: Array<{lang: string; content: string}>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,15 +47,17 @@ function parseVideoMimeType(localRefPath: string): string {
|
||||
* Convert ComposerState to server Draft format for saving.
|
||||
* Returns both the draft and a map of localRef paths to their source paths.
|
||||
*/
|
||||
export function composerStateToDraft(state: ComposerState): {
|
||||
export async function composerStateToDraft(state: ComposerState): Promise<{
|
||||
draft: AppBskyDraftDefs.Draft
|
||||
localRefPaths: Map<string, string>
|
||||
} {
|
||||
}> {
|
||||
const localRefPaths = new Map<string, string>()
|
||||
|
||||
const posts: AppBskyDraftDefs.DraftPost[] = state.thread.posts.map(post => {
|
||||
return postDraftToServerPost(post, localRefPaths)
|
||||
})
|
||||
const posts: AppBskyDraftDefs.DraftPost[] = await Promise.all(
|
||||
state.thread.posts.map(post => {
|
||||
return postDraftToServerPost(post, localRefPaths)
|
||||
}),
|
||||
)
|
||||
|
||||
// Convert threadgate settings to server format
|
||||
const threadgateAllow: AppBskyDraftDefs.Draft['threadgateAllow'] = []
|
||||
@@ -92,10 +95,10 @@ export function composerStateToDraft(state: ComposerState): {
|
||||
/**
|
||||
* Convert a single PostDraft to server DraftPost format.
|
||||
*/
|
||||
function postDraftToServerPost(
|
||||
async function postDraftToServerPost(
|
||||
post: PostDraft,
|
||||
localRefPaths: Map<string, string>,
|
||||
): AppBskyDraftDefs.DraftPost {
|
||||
): Promise<AppBskyDraftDefs.DraftPost> {
|
||||
const draftPost: AppBskyDraftDefs.DraftPost = {
|
||||
$type: 'app.bsky.draft.defs#draftPost',
|
||||
text: post.richtext.text,
|
||||
@@ -117,7 +120,7 @@ function postDraftToServerPost(
|
||||
localRefPaths,
|
||||
)
|
||||
} else if (post.embed.media.type === 'video') {
|
||||
const video = serializeVideo(post.embed.media.video, localRefPaths)
|
||||
const video = await serializeVideo(post.embed.media.video, localRefPaths)
|
||||
if (video) {
|
||||
draftPost.embedVideos = [video]
|
||||
}
|
||||
@@ -192,10 +195,10 @@ function serializeImages(
|
||||
* Serialize video to server format with localRef path.
|
||||
* The localRef path encodes the mime type: `video:${mimeType}:${nanoid()}`
|
||||
*/
|
||||
function serializeVideo(
|
||||
async function serializeVideo(
|
||||
videoState: VideoState,
|
||||
localRefPaths: Map<string, string>,
|
||||
): AppBskyDraftDefs.DraftEmbedVideo | undefined {
|
||||
): Promise<AppBskyDraftDefs.DraftEmbedVideo | undefined> {
|
||||
// Only save videos that have been compressed (have a video file)
|
||||
if (!videoState.video) {
|
||||
return undefined
|
||||
@@ -206,6 +209,19 @@ function serializeVideo(
|
||||
const localRefPath = `video:${mimeType}:${nanoid()}`
|
||||
localRefPaths.set(localRefPath, videoState.video.uri)
|
||||
|
||||
// Read caption file contents as text
|
||||
const captions: AppBskyDraftDefs.DraftEmbedCaption[] = []
|
||||
for (const caption of videoState.captions) {
|
||||
if (caption.lang) {
|
||||
const content = await caption.file.text()
|
||||
captions.push({
|
||||
$type: 'app.bsky.draft.defs#draftEmbedCaption',
|
||||
lang: caption.lang,
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$type: 'app.bsky.draft.defs#draftEmbedVideo',
|
||||
localRef: {
|
||||
@@ -213,7 +229,7 @@ function serializeVideo(
|
||||
path: localRefPath,
|
||||
},
|
||||
alt: videoState.altText || undefined,
|
||||
// TODO: Add captions if needed
|
||||
captions: captions.length > 0 ? captions : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,12 +489,15 @@ export function draftToComposerPosts(
|
||||
videoUri,
|
||||
altText: vid.alt,
|
||||
mimeType,
|
||||
captionCount: vid.captions?.length ?? 0,
|
||||
})
|
||||
restoredVideos.set(index, {
|
||||
uri: videoUri,
|
||||
altText: vid.alt || '',
|
||||
mimeType,
|
||||
localRefPath: vid.localRef.path,
|
||||
captions:
|
||||
vid.captions?.map(c => ({lang: c.lang, content: c.content})) ?? [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function useSaveDraftMutation() {
|
||||
originalLocalRefs: Set<string> | undefined
|
||||
}> => {
|
||||
// Convert composer state to server draft format
|
||||
const {draft, localRefPaths} = composerStateToDraft(composerState)
|
||||
const {draft, localRefPaths} = await composerStateToDraft(composerState)
|
||||
|
||||
logger.debug('saving draft', {
|
||||
existingDraftId,
|
||||
|
||||
Reference in New Issue
Block a user