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:
@@ -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,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user