Compare commits

...

3 Commits

Author SHA1 Message Date
Eric Bailey 5b97aff9db WIP draft previews 2026-01-29 17:28:47 -06:00
Eric Bailey 9ea02e9f29 Remove warning logs for failed media loading 2026-01-29 16:21:14 -06:00
Eric Bailey 4cd8c02b96 Allow saving quote post drafts 2026-01-29 16:01:24 -06:00
3 changed files with 108 additions and 17 deletions
+1 -1
View File
@@ -408,7 +408,7 @@ async function resolveMedia(
return undefined
}
async function resolveRecord(
export async function resolveRecord(
agent: BskyAgent,
queryClient: QueryClient,
uri: string,
+103 -12
View File
@@ -1,13 +1,16 @@
/**
* Type converters for Draft API - convert between ComposerState and server Draft types.
*/
import {type AppBskyDraftDefs, RichText} from '@atproto/api'
import {type AppBskyDraftDefs, type AppBskyFeedDefs, AtUri, RichText} from '@atproto/api'
import {nanoid} from 'nanoid/non-secure'
import * as VideoThumbnails from 'expo-video-thumbnails'
import {resolveLink} from '#/lib/api/resolve'
import {getImageDim} from '#/lib/media/manip'
import {mimeToExt} from '#/lib/media/video/util'
import {type ComposerImage} from '#/state/gallery'
import {type Gif} from '#/state/queries/tenor'
import {createPublicAgent} from '#/state/session/agent'
import {
type ComposerState,
type EmbedDraft,
@@ -16,6 +19,7 @@ import {
import {type VideoState} from '#/view/com/composer/state/video'
import {logger} from './logger'
import {type DraftPostDisplay, type DraftSummary} from './schema'
import * as storage from './storage'
const TENOR_HOSTNAME = 'media.tenor.com'
@@ -140,15 +144,21 @@ async function postDraftToServerPost(
// Add quote record embed
if (post.embed.quote) {
draftPost.embedRecords = [
{
$type: 'app.bsky.draft.defs#draftEmbedRecord',
record: {
uri: post.embed.quote.uri,
cid: '', // We don't have the CID at draft time
const resolved = await resolveLink(
createPublicAgent(),
post.embed.quote.uri,
)
if (resolved && resolved.type === 'record') {
draftPost.embedRecords = [
{
$type: 'app.bsky.draft.defs#draftEmbedRecord',
record: {
uri: resolved.record.uri,
cid: resolved.record.cid,
},
},
},
]
]
}
}
// Add external link embed (only if no media, otherwise it's ignored)
@@ -277,10 +287,10 @@ function serializeGif(gifMedia: {
* Convert server DraftView to DraftSummary for list display.
* Also checks which media files exist locally.
*/
export function draftViewToSummary(
export async function draftViewToSummary(
view: AppBskyDraftDefs.DraftView,
localMediaExists: (path: string) => boolean,
): DraftSummary {
): Promise<DraftSummary> {
const firstPost = view.draft.posts[0]
const previewText = firstPost?.text?.slice(0, 100) || ''
@@ -288,6 +298,85 @@ export function draftViewToSummary(
let hasMedia = false
let hasMissingMedia = false
const postsWithEmbed = await Promise.all(
view.draft.posts.map(async (post, i) => {
let embed: AppBskyFeedDefs.PostView['embed']
if (post.embedImages) {
embed = {
$type: 'app.bsky.embed.images#view',
images: await Promise.all(
post.embedImages.map(async image => {
const exists = localMediaExists(image.localRef.path)
const uri = exists ? await storage.loadMediaFromLocal(image.localRef.path) : ''
return {
$type: 'app.bsky.embed.images#viewImage',
thumb: uri,
fullsize: uri,
alt: image.alt || '',
}
})
)
}
} else if (post.embedVideos) {
const video = post.embedVideos[0]
const thumbnail = await VideoThumbnails.getThumbnailAsync(video.localRef.path, {
time: 0,
quality: 0.2,
})
embed = {
$type: 'app.bsky.embed.video#view',
cid: 'lol',
playlist: 'lol',
thumbnail: thumbnail.uri,
presentation: 'default', // TODO what about GIFs
}
} else if (post.embedExternals) {
const ext = post.embedExternals[0]
const resolved = await resolveLink(
createPublicAgent(),
ext.uri,
)
if (resolved.type === 'external') {
embed = {
$type: 'app.bsky.embed.external#view',
external: {
$type: 'app.bsky.embed.external#viewExternal',
uri: resolved.uri,
title: resolved.title,
description: resolved.description,
thumb: resolved.thumb?.source.path,
}
}
}
}
if (post.embedRecords) {
const quote = post.embedRecords[0]
const urip = new AtUri(quote.record.uri)
const url = `https://bsky.app/profile/${urip.host}/post/${urip.rkey}`
const resolvedQuote = await resolveLink(
createPublicAgent(),
url,
)
if (resolvedQuote.type === 'record') {
embed = {
$type: 'app.bsky.embed.record#view',
record: {
...resolvedQuote.view,
$type: 'app.bsky.embed.record#viewRecord', // TODO map to other types
}
}
}
}
return {
text: post.text,
embed,
}
})
)
console.log({postsWithEmbed})
const posts: DraftPostDisplay[] = view.draft.posts.map((post, index) => {
const images: DraftPostDisplay['images'] = []
const videos: DraftPostDisplay['video'][] = []
@@ -536,7 +625,9 @@ export async function draftToComposerPosts(
// Restore quote embed
if (post.embedRecords && post.embedRecords.length > 0) {
const record = post.embedRecords[0]
embed.quote = {type: 'link', uri: record.record.uri}
const urip = new AtUri(record.record.uri)
const url = `https://bsky.app/profile/${urip.host}/post/${urip.rkey}`
embed.quote = {type: 'link', uri: url}
}
// Restore link embed (only if not a GIF)
@@ -28,9 +28,9 @@ export function useDraftsQuery() {
const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
return {
cursor: res.data.cursor,
drafts: res.data.drafts.map(view =>
drafts: await Promise.all(res.data.drafts.map(view =>
draftViewToSummary(view, path => storage.mediaExists(path)),
),
)),
}
},
initialPageParam: undefined as string | undefined,
@@ -55,7 +55,7 @@ export async function loadDraft(draft: AppBskyDraftDefs.Draft): Promise<{
const url = await storage.loadMediaFromLocal(img.localRef.path)
loadedMedia.set(img.localRef.path, url)
} catch (e) {
logger.warn('Failed to load draft image', {
logger.debug('Failed to load draft image', {
path: img.localRef.path,
error: e,
})
@@ -69,7 +69,7 @@ export async function loadDraft(draft: AppBskyDraftDefs.Draft): Promise<{
const url = await storage.loadMediaFromLocal(vid.localRef.path)
loadedMedia.set(vid.localRef.path, url)
} catch (e) {
logger.warn('Failed to load draft video', {
logger.debug('Failed to load draft video', {
path: vid.localRef.path,
error: e,
})