diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 8ff0be21bb..f1827201a1 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -461,7 +461,7 @@ export const ComposePost = ({ }) // Convert server draft to composer posts (videos returned separately) - const {posts, restoredVideos} = draftToComposerPosts( + const {posts, restoredVideos} = await draftToComposerPosts( draftSummary.draft, loadedMedia, ) diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index 3f6d7907a7..e329e4abba 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -4,6 +4,7 @@ import {type AppBskyDraftDefs, RichText} from '@atproto/api' import {nanoid} from 'nanoid/non-secure' +import {getImageDim} from '#/lib/media/manip' import {type ComposerImage} from '#/state/gallery' import {type Gif} from '#/state/queries/tenor' import { @@ -399,146 +400,170 @@ function parseGifFromUrl( * the compression and upload pipeline. The caller should handle the restoredVideos * by initiating video processing for each entry. */ -export function draftToComposerPosts( +export async function draftToComposerPosts( draft: AppBskyDraftDefs.Draft, loadedMedia: Map, -): {posts: PostDraft[]; restoredVideos: Map} { +): Promise<{posts: PostDraft[]; restoredVideos: Map}> { const restoredVideos = new Map() - const posts = draft.posts.map((post, index) => { - const richtext = new RichText({text: post.text || ''}) - richtext.detectFacetsWithoutResolution() + const posts = await Promise.all( + draft.posts.map(async (post, index) => { + const richtext = new RichText({text: post.text || ''}) + richtext.detectFacetsWithoutResolution() - const embed: EmbedDraft = { - quote: undefined, - link: undefined, - media: undefined, - } + const embed: EmbedDraft = { + quote: undefined, + link: undefined, + media: undefined, + } + + // Restore images + if (post.embedImages && post.embedImages.length > 0) { + const imagePromises = post.embedImages.map(async img => { + const path = loadedMedia.get(img.localRef.path) + if (!path) { + return null + } + + let width = 0 + let height = 0 + try { + const dims = await getImageDim(path) + width = dims.width + height = dims.height + } catch (e) { + logger.warn('Failed to get image dimensions', { + path, + error: e, + }) + } - // Restore images - if (post.embedImages && post.embedImages.length > 0) { - const images: ComposerImage[] = [] - for (const img of post.embedImages) { - const path = loadedMedia.get(img.localRef.path) - if (path) { logger.debug('restoring image with localRefPath', { localRefPath: img.localRef.path, loadedPath: path, + width, + height, }) - images.push({ + + return { alt: img.alt || '', // Preserve the original localRefPath for reuse when saving localRefPath: img.localRef.path, source: { id: nanoid(), path, - width: 0, // Will be recalculated when loaded - height: 0, - mime: 'image/jpeg', // Default, will be detected + width, + height, + mime: 'image/jpeg', }, + } as ComposerImage + }) + + const images = (await Promise.all(imagePromises)).filter( + (img): img is ComposerImage => img !== null, + ) + if (images.length > 0) { + embed.media = {type: 'images', images} + } + } + + // Restore GIF from external embed + if (post.embedExternals) { + for (const ext of post.embedExternals) { + const gifData = parseGifFromUrl(ext.uri) + if (gifData) { + // Reconstruct a Gif object with all required properties + const mediaObject = { + url: gifData.url, + dims: [gifData.width, gifData.height] as [number, number], + duration: 0, + size: 0, + } + embed.media = { + type: 'gif', + gif: { + id: '', + created: 0, + hasaudio: false, + hascaption: false, + flags: '', + tags: [], + title: '', + content_description: gifData.alt || '', + itemurl: '', + url: gifData.url, // Required for useResolveGifQuery + media_formats: { + gif: mediaObject, + tinygif: mediaObject, + preview: mediaObject, + }, + } as Gif, + alt: gifData.alt, + } + break + } + } + } + + // 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, + 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})) ?? + [], }) } } - if (images.length > 0) { - embed.media = {type: 'images', images} - } - } - // Restore GIF from external embed - if (post.embedExternals) { - for (const ext of post.embedExternals) { - const gifData = parseGifFromUrl(ext.uri) - if (gifData) { - // Reconstruct a Gif object with all required properties - const mediaObject = { - url: gifData.url, - dims: [gifData.width, gifData.height] as [number, number], - duration: 0, - size: 0, + // Restore quote embed + if (post.embedRecords && post.embedRecords.length > 0) { + const record = post.embedRecords[0] + embed.quote = {type: 'link', uri: record.record.uri} + } + + // Restore link embed (only if not a GIF) + if (post.embedExternals && !embed.media) { + for (const ext of post.embedExternals) { + const gifData = parseGifFromUrl(ext.uri) + if (!gifData) { + embed.link = {type: 'link', uri: ext.uri} + break } - embed.media = { - type: 'gif', - gif: { - id: '', - created: 0, - hasaudio: false, - hascaption: false, - flags: '', - tags: [], - title: '', - content_description: gifData.alt || '', - itemurl: '', - url: gifData.url, // Required for useResolveGifQuery - media_formats: { - gif: mediaObject, - tinygif: mediaObject, - preview: mediaObject, - }, - } as Gif, - alt: gifData.alt, - } - break } } - } - // 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, - 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})) ?? [], - }) - } - } - - // Restore quote embed - if (post.embedRecords && post.embedRecords.length > 0) { - const record = post.embedRecords[0] - embed.quote = {type: 'link', uri: record.record.uri} - } - - // Restore link embed (only if not a GIF) - if (post.embedExternals && !embed.media) { - for (const ext of post.embedExternals) { - const gifData = parseGifFromUrl(ext.uri) - if (!gifData) { - embed.link = {type: 'link', uri: ext.uri} - break + // Parse labels + const labels: string[] = [] + if (post.labels && 'values' in post.labels) { + for (const val of post.labels.values) { + labels.push(val.val) } } - } - // Parse labels - const labels: string[] = [] - if (post.labels && 'values' in post.labels) { - for (const val of post.labels.values) { - labels.push(val.val) - } - } - - return { - id: `draft-post-${index}`, - richtext, - shortenedGraphemeLength: richtext.graphemeLength, - labels, - embed, - } as PostDraft - }) + return { + id: `draft-post-${index}`, + richtext, + shortenedGraphemeLength: richtext.graphemeLength, + labels, + embed, + } as PostDraft + }), + ) return {posts, restoredVideos} }