Draft previews (#9803)

* Send deviceId and platform

* Add deviceId and deviceName to drafts, skip loading media for other devies

* WIP new preview

* show rich text in drafts list

(cherry picked from commit fb70d53d59)

* New draft preview UI

* Tighten up spacing in draft list

* Add i18n comments

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Eric Bailey
2026-01-30 17:00:54 -06:00
committed by GitHub
parent 45d6e50eb4
commit 748706daab
11 changed files with 407 additions and 224 deletions
+45 -26
View File
@@ -5,6 +5,7 @@ import {type AppBskyDraftDefs, AtUri, RichText} from '@atproto/api'
import {nanoid} from 'nanoid/non-secure'
import {resolveLink} from '#/lib/api/resolve'
import {getDeviceName} from '#/lib/deviceName'
import {getImageDim} from '#/lib/media/manip'
import {mimeToExt} from '#/lib/media/video/util'
import {type ComposerImage} from '#/state/gallery'
@@ -17,8 +18,11 @@ import {
type PostDraft,
} from '#/view/com/composer/state/composer'
import {type VideoState} from '#/view/com/composer/state/video'
import {type AnalyticsContextType} from '#/analytics'
import {getDeviceId} from '#/analytics/identifiers'
import {logger} from './logger'
import {type DraftPostDisplay, type DraftSummary} from './schema'
import * as storage from './storage'
const TENOR_HOSTNAME = 'media.tenor.com'
@@ -66,6 +70,8 @@ export async function composerStateToDraft(state: ComposerState): Promise<{
const draft: AppBskyDraftDefs.Draft = {
$type: 'app.bsky.draft.defs#draft',
deviceId: getDeviceId(),
deviceName: getDeviceName().slice(0, 100), // max length of 100 in lex
posts,
threadgateAllow: threadgateAllowUISettingToAllowRecordValue(
state.thread.threadgate,
@@ -265,16 +271,24 @@ function serializeGif(gifMedia: {
* Convert server DraftView to DraftSummary for list display.
* Also checks which media files exist locally.
*/
export function draftViewToSummary(
view: AppBskyDraftDefs.DraftView,
localMediaExists: (path: string) => boolean,
): DraftSummary {
const firstPost = view.draft.posts[0]
const previewText = firstPost?.text?.slice(0, 100) || ''
let mediaCount = 0
let hasMedia = false
let hasMissingMedia = false
export function draftViewToSummary({
view,
analytics,
}: {
view: AppBskyDraftDefs.DraftView
analytics: AnalyticsContextType
}): DraftSummary {
const meta = {
isOriginatingDevice: view.draft.deviceId === getDeviceId(),
postCount: view.draft.posts.length,
// minus anchor post
replyCount: view.draft.posts.length - 1,
hasMedia: false,
hasMissingMedia: false,
mediaCount: 0,
hasQuotes: false,
quoteCount: 0,
}
const posts: DraftPostDisplay[] = view.draft.posts.map((post, index) => {
const images: DraftPostDisplay['images'] = []
@@ -284,11 +298,11 @@ export function draftViewToSummary(
// Process images
if (post.embedImages) {
for (const img of post.embedImages) {
mediaCount++
hasMedia = true
const exists = localMediaExists(img.localRef.path)
meta.mediaCount++
meta.hasMedia = true
const exists = storage.mediaExists(img.localRef.path)
if (!exists) {
hasMissingMedia = true
meta.hasMissingMedia = true
}
images.push({
localPath: img.localRef.path,
@@ -301,11 +315,11 @@ export function draftViewToSummary(
// Process videos
if (post.embedVideos) {
for (const vid of post.embedVideos) {
mediaCount++
hasMedia = true
const exists = localMediaExists(vid.localRef.path)
meta.mediaCount++
meta.hasMedia = true
const exists = storage.mediaExists(vid.localRef.path)
if (!exists) {
hasMissingMedia = true
meta.hasMissingMedia = true
}
videos.push({
localPath: vid.localRef.path,
@@ -320,13 +334,18 @@ export function draftViewToSummary(
for (const ext of post.embedExternals) {
const gifData = parseGifFromUrl(ext.uri)
if (gifData) {
mediaCount++
hasMedia = true
meta.mediaCount++
meta.hasMedia = true
gif = gifData
}
}
}
if (post.embedRecords && post.embedRecords.length > 0) {
meta.quoteCount += post.embedRecords.length
meta.hasQuotes = true
}
return {
id: `post-${index}`,
text: post.text || '',
@@ -336,17 +355,17 @@ export function draftViewToSummary(
}
})
if (meta.isOriginatingDevice && meta.hasMissingMedia) {
analytics.logger.warn(`Draft is missing media on originating device`, {})
}
return {
id: view.id,
draft: view.draft,
previewText,
hasMedia,
hasMissingMedia,
mediaCount,
postCount: view.draft.posts.length,
createdAt: view.createdAt,
updatedAt: view.updatedAt,
draft: view.draft,
posts,
meta,
}
}