get image aspect ratio when restoring

This commit is contained in:
Samuel Newman
2026-01-28 11:07:18 +02:00
parent 39024e6a36
commit fe57c73c38
2 changed files with 138 additions and 113 deletions
+1 -1
View File
@@ -461,7 +461,7 @@ export const ComposePost = ({
}) })
// Convert server draft to composer posts (videos returned separately) // Convert server draft to composer posts (videos returned separately)
const {posts, restoredVideos} = draftToComposerPosts( const {posts, restoredVideos} = await draftToComposerPosts(
draftSummary.draft, draftSummary.draft,
loadedMedia, loadedMedia,
) )
+137 -112
View File
@@ -4,6 +4,7 @@
import {type AppBskyDraftDefs, RichText} from '@atproto/api' import {type AppBskyDraftDefs, RichText} from '@atproto/api'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {getImageDim} from '#/lib/media/manip'
import {type ComposerImage} from '#/state/gallery' import {type ComposerImage} from '#/state/gallery'
import {type Gif} from '#/state/queries/tenor' import {type Gif} from '#/state/queries/tenor'
import { import {
@@ -399,146 +400,170 @@ function parseGifFromUrl(
* the compression and upload pipeline. The caller should handle the restoredVideos * the compression and upload pipeline. The caller should handle the restoredVideos
* by initiating video processing for each entry. * by initiating video processing for each entry.
*/ */
export function draftToComposerPosts( export async function draftToComposerPosts(
draft: AppBskyDraftDefs.Draft, draft: AppBskyDraftDefs.Draft,
loadedMedia: Map<string, string>, loadedMedia: Map<string, string>,
): {posts: PostDraft[]; restoredVideos: Map<number, RestoredVideo>} { ): Promise<{posts: PostDraft[]; restoredVideos: Map<number, RestoredVideo>}> {
const restoredVideos = new Map<number, RestoredVideo>() const restoredVideos = new Map<number, RestoredVideo>()
const posts = draft.posts.map((post, index) => { const posts = await Promise.all(
const richtext = new RichText({text: post.text || ''}) draft.posts.map(async (post, index) => {
richtext.detectFacetsWithoutResolution() const richtext = new RichText({text: post.text || ''})
richtext.detectFacetsWithoutResolution()
const embed: EmbedDraft = { const embed: EmbedDraft = {
quote: undefined, quote: undefined,
link: undefined, link: undefined,
media: 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', { logger.debug('restoring image with localRefPath', {
localRefPath: img.localRef.path, localRefPath: img.localRef.path,
loadedPath: path, loadedPath: path,
width,
height,
}) })
images.push({
return {
alt: img.alt || '', alt: img.alt || '',
// Preserve the original localRefPath for reuse when saving // Preserve the original localRefPath for reuse when saving
localRefPath: img.localRef.path, localRefPath: img.localRef.path,
source: { source: {
id: nanoid(), id: nanoid(),
path, path,
width: 0, // Will be recalculated when loaded width,
height: 0, height,
mime: 'image/jpeg', // Default, will be detected 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 // Restore quote embed
if (post.embedExternals) { if (post.embedRecords && post.embedRecords.length > 0) {
for (const ext of post.embedExternals) { const record = post.embedRecords[0]
const gifData = parseGifFromUrl(ext.uri) embed.quote = {type: 'link', uri: record.record.uri}
if (gifData) { }
// Reconstruct a Gif object with all required properties
const mediaObject = { // Restore link embed (only if not a GIF)
url: gifData.url, if (post.embedExternals && !embed.media) {
dims: [gifData.width, gifData.height] as [number, number], for (const ext of post.embedExternals) {
duration: 0, const gifData = parseGifFromUrl(ext.uri)
size: 0, 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) // Parse labels
if (post.embedVideos && post.embedVideos.length > 0) { const labels: string[] = []
const vid = post.embedVideos[0] if (post.labels && 'values' in post.labels) {
const videoUri = loadedMedia.get(vid.localRef.path) for (const val of post.labels.values) {
if (videoUri) { labels.push(val.val)
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 return {
const labels: string[] = [] id: `draft-post-${index}`,
if (post.labels && 'values' in post.labels) { richtext,
for (const val of post.labels.values) { shortenedGraphemeLength: richtext.graphemeLength,
labels.push(val.val) labels,
} embed,
} } as PostDraft
}),
return { )
id: `draft-post-${index}`,
richtext,
shortenedGraphemeLength: richtext.graphemeLength,
labels,
embed,
} as PostDraft
})
return {posts, restoredVideos} return {posts, restoredVideos}
} }