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