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(
async (draftSummary: DraftSummary) => {
// Load full draft from server with media
const result = await loadDraft(draftSummary.id)
if (!result) return
const {draft, loadedMedia} = result
// Load local media files for the draft
const {loadedMedia} = await loadDraft(draftSummary.draft)
// 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)
composerDispatch({
type: 'restore_from_draft',
draftId: draftSummary.id,
posts,
threadgateAllow: draft.threadgateAllow,
postgateEmbeddingRules: draft.postgateEmbeddingRules,
threadgateAllow: draftSummary.draft.threadgateAllow,
postgateEmbeddingRules: draftSummary.draft.postgateEmbeddingRules,
loadedMedia,
})
},
+3 -3
View File
@@ -30,15 +30,15 @@ export function DraftItem({
}: {
draft: DraftSummary
onSelect: (draft: DraftSummary) => void
onDelete: (draftId: string) => void
onDelete: (draft: DraftSummary) => void
}) {
const {_} = useLingui()
const t = useTheme()
const discardPromptControl = Prompt.usePromptControl()
const handleDelete = useCallback(() => {
onDelete(draft.id)
}, [onDelete, draft.id])
onDelete(draft)
}, [onDelete, draft])
return (
<>
@@ -41,8 +41,8 @@ export function DraftsListDialog({
)
const handleDeleteDraft = useCallback(
(draftId: string) => {
deleteDraft(draftId)
(draftSummary: DraftSummary) => {
deleteDraft({draftId: draftSummary.id, draft: draftSummary.draft})
},
[deleteDraft],
)
+1 -1
View File
@@ -289,12 +289,12 @@ export function draftViewToSummary(
return {
id: view.id,
draft: view.draft,
previewText,
hasMedia,
hasMissingMedia,
mediaCount,
postCount: view.draft.posts.length,
isReply: false, // Reply drafts not supported
updatedAt: view.updatedAt,
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() {
const agent = useAgent()
return useCallback(
async (
draftId: string,
draft: AppBskyDraftDefs.Draft,
): Promise<{
draft: AppBskyDraftDefs.Draft
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
const loadedMedia = new Map<string, string>()
for (const post of draftView.draft.posts) {
for (const post of draft.posts) {
// Load images
if (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() {
const agent = useAgent()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (draftId: string) => {
// First fetch the draft to get media paths for cleanup
const res = await agent.app.bsky.draft.getDrafts({})
const draftView = res.data.drafts.find(d => d.id === draftId)
if (draftView) {
// Delete local media files
for (const post of draftView.draft.posts) {
if (post.embedImages) {
for (const img of post.embedImages) {
await storage.deleteMediaFromLocal(img.localRef.path)
}
mutationFn: async ({
draftId,
draft,
}: {
draftId: string
draft: AppBskyDraftDefs.Draft
}) => {
// Delete local media files
for (const post of draft.posts) {
if (post.embedImages) {
for (const img of post.embedImages) {
await storage.deleteMediaFromLocal(img.localRef.path)
}
if (post.embedVideos) {
for (const vid of post.embedVideos) {
await storage.deleteMediaFromLocal(vid.localRef.path)
}
}
if (post.embedVideos) {
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.
* Server draft types come from @atproto/api.
*/
import {type AppBskyDraftDefs} from '@atproto/api'
/**
* Reference to locally cached media file for display
@@ -47,6 +48,8 @@ export type DraftPostDisplay = {
*/
export type DraftSummary = {
id: string
/** The full draft data from the server */
draft: AppBskyDraftDefs.Draft
/** First ~100 chars of first post */
previewText: string
/** Whether the draft has media */
@@ -57,8 +60,6 @@ export type DraftSummary = {
mediaCount: number
/** Number of posts in thread */
postCount: number
/** Whether this is a reply (always false - replies not supported) */
isReply: boolean
/** ISO timestamp of last update */
updatedAt: string
/** All posts in the draft for full display */