refactor: pass full draft data instead of re-fetching

The useLoadDraft and useDeleteDraftMutation hooks were fetching drafts
via getDrafts() to look up a draft by ID. This was problematic because
getDrafts is paginated, so drafts not on the first page wouldn't be
found.

Changes:
- Add full Draft object to DraftSummary type
- useLoadDraft now takes Draft directly (only loads local media)
- useDeleteDraftMutation now takes {draftId, draft} to avoid re-fetch
- Update DraftItem and DraftsListDialog to pass full draft data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-16 16:42:59 +02:00
parent ca3fcfe89c
commit 9fcaa8ff39
6 changed files with 39 additions and 50 deletions
+5 -8
View File
@@ -327,22 +327,19 @@ export const ComposePost = ({
const handleSelectDraft = React.useCallback( const handleSelectDraft = React.useCallback(
async (draftSummary: DraftSummary) => { async (draftSummary: DraftSummary) => {
// Load full draft from server with media // Load local media files for the draft
const result = await loadDraft(draftSummary.id) const {loadedMedia} = await loadDraft(draftSummary.draft)
if (!result) return
const {draft, loadedMedia} = result
// Convert server draft to composer posts // Convert server draft to composer posts
const posts = draftToComposerPosts(draft, loadedMedia) const posts = draftToComposerPosts(draftSummary.draft, loadedMedia)
// Dispatch restore action (this also sets draftId in state) // Dispatch restore action (this also sets draftId in state)
composerDispatch({ composerDispatch({
type: 'restore_from_draft', type: 'restore_from_draft',
draftId: draftSummary.id, draftId: draftSummary.id,
posts, posts,
threadgateAllow: draft.threadgateAllow, threadgateAllow: draftSummary.draft.threadgateAllow,
postgateEmbeddingRules: draft.postgateEmbeddingRules, postgateEmbeddingRules: draftSummary.draft.postgateEmbeddingRules,
loadedMedia, loadedMedia,
}) })
}, },
+3 -3
View File
@@ -30,15 +30,15 @@ export function DraftItem({
}: { }: {
draft: DraftSummary draft: DraftSummary
onSelect: (draft: DraftSummary) => void onSelect: (draft: DraftSummary) => void
onDelete: (draftId: string) => void onDelete: (draft: DraftSummary) => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const discardPromptControl = Prompt.usePromptControl() const discardPromptControl = Prompt.usePromptControl()
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
onDelete(draft.id) onDelete(draft)
}, [onDelete, draft.id]) }, [onDelete, draft])
return ( return (
<> <>
@@ -41,8 +41,8 @@ export function DraftsListDialog({
) )
const handleDeleteDraft = useCallback( const handleDeleteDraft = useCallback(
(draftId: string) => { (draftSummary: DraftSummary) => {
deleteDraft(draftId) deleteDraft({draftId: draftSummary.id, draft: draftSummary.draft})
}, },
[deleteDraft], [deleteDraft],
) )
+1 -1
View File
@@ -289,12 +289,12 @@ export function draftViewToSummary(
return { return {
id: view.id, id: view.id,
draft: view.draft,
previewText, previewText,
hasMedia, hasMedia,
hasMissingMedia, hasMissingMedia,
mediaCount, mediaCount,
postCount: view.draft.posts.length, postCount: view.draft.posts.length,
isReply: false, // Reply drafts not supported
updatedAt: view.updatedAt, updatedAt: view.updatedAt,
posts, posts,
} }
+25 -34
View File
@@ -40,29 +40,19 @@ export function useDraftsQuery() {
} }
/** /**
* Hook to load a specific draft for editing * Hook to load a draft's local media for editing.
* Takes the full Draft object (from DraftSummary) to avoid re-fetching.
*/ */
export function useLoadDraft() { export function useLoadDraft() {
const agent = useAgent()
return useCallback( return useCallback(
async ( async (
draftId: string, draft: AppBskyDraftDefs.Draft,
): Promise<{ ): Promise<{
draft: AppBskyDraftDefs.Draft
loadedMedia: Map<string, string> loadedMedia: Map<string, string>
} | null> => { }> => {
// Fetch the draft from server
const res = await agent.app.bsky.draft.getDrafts({})
const draftView = res.data.drafts.find(d => d.id === draftId)
if (!draftView) {
return null
}
// Load local media files // Load local media files
const loadedMedia = new Map<string, string>() const loadedMedia = new Map<string, string>()
for (const post of draftView.draft.posts) { for (const post of draft.posts) {
// Load images // Load images
if (post.embedImages) { if (post.embedImages) {
for (const img of post.embedImages) { for (const img of post.embedImages) {
@@ -93,9 +83,9 @@ export function useLoadDraft() {
} }
} }
return {draft: draftView.draft, loadedMedia} return {loadedMedia}
}, },
[agent], [],
) )
} }
@@ -158,30 +148,31 @@ export function useSaveDraftMutation() {
} }
/** /**
* Hook to delete a draft * Hook to delete a draft.
* Takes the full draft data to avoid re-fetching for media cleanup.
*/ */
export function useDeleteDraftMutation() { export function useDeleteDraftMutation() {
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: async (draftId: string) => { mutationFn: async ({
// First fetch the draft to get media paths for cleanup draftId,
const res = await agent.app.bsky.draft.getDrafts({}) draft,
const draftView = res.data.drafts.find(d => d.id === draftId) }: {
draftId: string
if (draftView) { draft: AppBskyDraftDefs.Draft
// Delete local media files }) => {
for (const post of draftView.draft.posts) { // Delete local media files
if (post.embedImages) { for (const post of draft.posts) {
for (const img of post.embedImages) { if (post.embedImages) {
await storage.deleteMediaFromLocal(img.localRef.path) for (const img of post.embedImages) {
} await storage.deleteMediaFromLocal(img.localRef.path)
} }
if (post.embedVideos) { }
for (const vid of post.embedVideos) { if (post.embedVideos) {
await storage.deleteMediaFromLocal(vid.localRef.path) for (const vid of post.embedVideos) {
} await storage.deleteMediaFromLocal(vid.localRef.path)
} }
} }
} }
+3 -2
View File
@@ -2,6 +2,7 @@
* Types for draft display and local media tracking. * Types for draft display and local media tracking.
* Server draft types come from @atproto/api. * Server draft types come from @atproto/api.
*/ */
import {type AppBskyDraftDefs} from '@atproto/api'
/** /**
* Reference to locally cached media file for display * Reference to locally cached media file for display
@@ -47,6 +48,8 @@ export type DraftPostDisplay = {
*/ */
export type DraftSummary = { export type DraftSummary = {
id: string id: string
/** The full draft data from the server */
draft: AppBskyDraftDefs.Draft
/** First ~100 chars of first post */ /** First ~100 chars of first post */
previewText: string previewText: string
/** Whether the draft has media */ /** Whether the draft has media */
@@ -57,8 +60,6 @@ export type DraftSummary = {
mediaCount: number mediaCount: number
/** Number of posts in thread */ /** Number of posts in thread */
postCount: number postCount: number
/** Whether this is a reply (always false - replies not supported) */
isReply: boolean
/** ISO timestamp of last update */ /** ISO timestamp of last update */
updatedAt: string updatedAt: string
/** All posts in the draft for full display */ /** All posts in the draft for full display */