Draft storage abstraction layer

This commit is contained in:
Alex Benzer
2025-12-22 13:24:37 -08:00
committed by Samuel Newman
parent 22e0edbdaa
commit bb9d7f48a7
5 changed files with 403 additions and 285 deletions
+1
View File
@@ -0,0 +1 @@
export {type DraftItem, draftsStorage} from './storage'
+253
View File
@@ -0,0 +1,253 @@
import {logger} from '#/logger'
import {account, type ComposerDraft} from '#/storage'
const MAX_DRAFT_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
export type DraftItem = {
id: string
draft: ComposerDraft
}
/**
* Abstraction layer for draft storage operations.
*
* Currently backed by MMKV (local storage), but designed with an async API
* to make future migration to a backend key-value store easier.
*
* All operations are scoped to a specific account (DID).
*
* Note: Sync methods (*Sync) are provided for cases where async isn't possible
* (e.g., React initial state). When migrating to a backend, these will need
* to be refactored to use async patterns (loading states, suspense, etc.).
*/
export const draftsStorage = {
/**
* Get a single draft by ID
*/
async getDraft(did: string, draftId: string): Promise<ComposerDraft | null> {
try {
const allDrafts = account.get([did, 'composerDrafts'])
if (!allDrafts) return null
const draft = allDrafts[draftId]
if (!draft) return null
// Validate version and age
if (draft.version !== 1) {
logger.warn('Incompatible draft version', {
draftId,
version: draft.version,
})
return null
}
if (Date.now() - draft.timestamp > MAX_DRAFT_AGE_MS) {
logger.debug('Draft too old', {draftId})
return null
}
return draft
} catch (e) {
logger.error('Failed to get draft', {error: e, draftId})
return null
}
},
/**
* Get all drafts for an account, sorted by timestamp (newest first)
*/
async getAllDrafts(did: string): Promise<DraftItem[]> {
try {
const allDrafts = account.get([did, 'composerDrafts'])
if (!allDrafts) return []
const now = Date.now()
const items: DraftItem[] = []
for (const [id, draft] of Object.entries(allDrafts)) {
// Skip invalid or too old drafts
if (draft.version !== 1) continue
if (now - draft.timestamp > MAX_DRAFT_AGE_MS) continue
items.push({id, draft})
}
// Sort by timestamp, newest first
items.sort((a, b) => b.draft.timestamp - a.draft.timestamp)
return items
} catch (e) {
logger.error('Failed to get drafts list', {error: e})
return []
}
},
/**
* Save a draft (create or update)
*/
async saveDraft(
did: string,
draftId: string,
draft: ComposerDraft,
): Promise<void> {
try {
const allDrafts = account.get([did, 'composerDrafts']) ?? {}
account.set([did, 'composerDrafts'], {
...allDrafts,
[draftId]: draft,
})
logger.debug('Draft saved', {draftId})
} catch (e) {
logger.error('Failed to save draft', {error: e, draftId})
throw e
}
},
/**
* Delete a single draft
*/
async deleteDraft(did: string, draftId: string): Promise<void> {
try {
const allDrafts = account.get([did, 'composerDrafts'])
if (!allDrafts || !allDrafts[draftId]) return
const remainingDrafts: Record<string, ComposerDraft> = {}
for (const [id, draft] of Object.entries(allDrafts)) {
if (id !== draftId) {
remainingDrafts[id] = draft
}
}
if (Object.keys(remainingDrafts).length > 0) {
account.set([did, 'composerDrafts'], remainingDrafts)
} else {
account.remove([did, 'composerDrafts'])
}
logger.debug('Draft deleted', {draftId})
} catch (e) {
logger.error('Failed to delete draft', {error: e, draftId})
throw e
}
},
/**
* Check if a draft exists
*/
async hasDraft(did: string, draftId: string): Promise<boolean> {
const draft = await this.getDraft(did, draftId)
return draft !== null
},
/**
* Get count of valid drafts
*/
async getDraftsCount(did: string): Promise<number> {
const drafts = await this.getAllDrafts(did)
return drafts.length
},
/**
* Clean up old/invalid drafts
*/
async cleanupOldDrafts(did: string): Promise<number> {
try {
const allDrafts = account.get([did, 'composerDrafts'])
if (!allDrafts) return 0
const now = Date.now()
const remainingDrafts: Record<string, ComposerDraft> = {}
let removedCount = 0
for (const [id, draft] of Object.entries(allDrafts)) {
if (draft.version === 1 && now - draft.timestamp <= MAX_DRAFT_AGE_MS) {
remainingDrafts[id] = draft
} else {
removedCount++
}
}
if (removedCount > 0) {
if (Object.keys(remainingDrafts).length > 0) {
account.set([did, 'composerDrafts'], remainingDrafts)
} else {
account.remove([did, 'composerDrafts'])
}
logger.debug('Cleaned up old drafts', {removedCount})
}
return removedCount
} catch (e) {
logger.error('Failed to cleanup old drafts', {error: e})
return 0
}
},
// ============================================
// Synchronous methods (for use in contexts where async isn't possible)
// These will need refactoring when migrating to a backend store
// ============================================
/**
* Get a single draft by ID (synchronous)
* Use this only when async isn't possible (e.g., React initial state)
*/
getDraftSync(did: string, draftId: string): ComposerDraft | null {
try {
const allDrafts = account.get([did, 'composerDrafts'])
if (!allDrafts) return null
const draft = allDrafts[draftId]
if (!draft) return null
// Validate version and age
if (draft.version !== 1) {
logger.warn('Incompatible draft version', {
draftId,
version: draft.version,
})
return null
}
if (Date.now() - draft.timestamp > MAX_DRAFT_AGE_MS) {
logger.debug('Draft too old', {draftId})
return null
}
return draft
} catch (e) {
logger.error('Failed to get draft (sync)', {error: e, draftId})
return null
}
},
/**
* Get all drafts for an account (synchronous)
* Use this only when async isn't possible
*/
getAllDraftsSync(did: string): DraftItem[] {
try {
const allDrafts = account.get([did, 'composerDrafts'])
if (!allDrafts) return []
const now = Date.now()
const items: DraftItem[] = []
for (const [id, draft] of Object.entries(allDrafts)) {
// Skip invalid or too old drafts
if (draft.version !== 1) continue
if (now - draft.timestamp > MAX_DRAFT_AGE_MS) continue
items.push({id, draft})
}
// Sort by timestamp, newest first
items.sort((a, b) => b.draft.timestamp - a.draft.timestamp)
return items
} catch (e) {
logger.error('Failed to get drafts list (sync)', {error: e})
return []
}
},
}
+53 -79
View File
@@ -78,6 +78,7 @@ import {colors} from '#/lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {draftsStorage} from '#/state/drafts'
import {emitPostCreated} from '#/state/events' import {emitPostCreated} from '#/state/events'
import { import {
type ComposerImage, type ComposerImage,
@@ -130,7 +131,6 @@ import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {account} from '#/storage'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import {DraftsView} from './DraftsDialog' import {DraftsView} from './DraftsDialog'
import {PostLanguageSelect} from './select-language/PostLanguageSelect' import {PostLanguageSelect} from './select-language/PostLanguageSelect'
@@ -302,46 +302,27 @@ export const ComposePost = ({
// If a draftId is provided, load that specific draft // If a draftId is provided, load that specific draft
if (initDraftId) { if (initDraftId) {
try { const draft = draftsStorage.getDraftSync(currentAccount.did, initDraftId)
const allDrafts = account.get([currentAccount.did, 'composerDrafts']) if (!draft) return null
if (!allDrafts) return null
const draft = allDrafts[initDraftId] logger.info('Composer: loading draft by ID', {
if (!draft) return null draftId: initDraftId,
textLength: draft.thread.posts[0]?.text.length || 0,
})
if (draft.version !== 1) return null // Construct video URLs from blobRefs if we have videos in the draft
const parsed = JSON.parse(JSON.stringify(draft)) // Deep clone
// Check age if (parsed.thread?.posts) {
const age = Date.now() - draft.timestamp parsed.thread.posts = parsed.thread.posts.map((post: any) => {
if (age > 7 * 24 * 60 * 60 * 1000) { if (post.embed?.video?.blobRef?.ref?.$link) {
return null const cid = post.embed.video.blobRef.ref.$link
} post.embed.video.uri = `https://video.bsky.app/watch/${encodeURIComponent(currentAccount.did)}/${cid}/playlist.m3u8`
}
logger.info('Composer: loading draft by ID', { return post
draftId: initDraftId,
textLength: draft.thread.posts[0]?.text.length || 0,
}) })
// Construct video URLs from blobRefs if we have videos in the draft
const parsed = JSON.parse(JSON.stringify(draft)) // Deep clone
if (parsed.thread?.posts) {
parsed.thread.posts = parsed.thread.posts.map((post: any) => {
if (post.embed?.video?.blobRef?.ref?.$link) {
const cid = post.embed.video.blobRef.ref.$link
post.embed.video.uri = `https://video.bsky.app/watch/${encodeURIComponent(currentAccount.did)}/${cid}/playlist.m3u8`
}
return post
})
}
return parsed
} catch (e) {
logger.error('Failed to load draft by ID', {
error: e,
draftId: initDraftId,
})
return null
} }
return parsed
} }
return null return null
@@ -424,51 +405,44 @@ export const ComposePost = ({
(selectedDraftId: string) => { (selectedDraftId: string) => {
if (!currentAccount) return if (!currentAccount) return
try { const draft = draftsStorage.getDraftSync(
const allDrafts = account.get([currentAccount.did, 'composerDrafts']) currentAccount.did,
if (!allDrafts) return selectedDraftId,
)
if (!draft) return
const draft = allDrafts[selectedDraftId] // Deep clone and construct video URLs if needed
if (!draft || draft.version !== 1) return const parsed = JSON.parse(JSON.stringify(draft))
if (parsed.thread?.posts) {
// Deep clone and construct video URLs if needed parsed.thread.posts = parsed.thread.posts.map((post: any) => {
const parsed = JSON.parse(JSON.stringify(draft)) if (post.embed?.video?.blobRef?.ref?.$link) {
if (parsed.thread?.posts) { const cid = post.embed.video.blobRef.ref.$link
parsed.thread.posts = parsed.thread.posts.map((post: any) => { post.embed.video.uri = `https://video.bsky.app/watch/${encodeURIComponent(currentAccount.did)}/${cid}/playlist.m3u8`
if (post.embed?.video?.blobRef?.ref?.$link) { }
const cid = post.embed.video.blobRef.ref.$link return post
post.embed.video.uri = `https://video.bsky.app/watch/${encodeURIComponent(currentAccount.did)}/${cid}/playlist.m3u8`
}
return post
})
}
// Convert to ComposerState
const newState = createComposerState({
initText: undefined,
initMention: undefined,
initImageUris: undefined,
initQuoteUri: undefined,
initInteractionSettings: preferences?.postInteractionSettings,
initDraft: parsed,
})
// Update draft tracking state
setCurrentDraftId(selectedDraftId)
setIsEditingExistingDraft(true)
setLoadedDraftSnapshot(serializeStateForComparison(newState))
// Load the draft and switch back to compose mode
composerDispatch({type: 'load_draft', draft: newState})
setViewMode('compose')
logger.info('Loaded draft into composer', {draftId: selectedDraftId})
} catch (e) {
logger.error('Failed to load draft', {
error: e,
draftId: selectedDraftId,
}) })
} }
// Convert to ComposerState
const newState = createComposerState({
initText: undefined,
initMention: undefined,
initImageUris: undefined,
initQuoteUri: undefined,
initInteractionSettings: preferences?.postInteractionSettings,
initDraft: parsed,
})
// Update draft tracking state
setCurrentDraftId(selectedDraftId)
setIsEditingExistingDraft(true)
setLoadedDraftSnapshot(serializeStateForComparison(newState))
// Load the draft and switch back to compose mode
composerDispatch({type: 'load_draft', draft: newState})
setViewMode('compose')
logger.info('Loaded draft into composer', {draftId: selectedDraftId})
}, },
[currentAccount, preferences?.postInteractionSettings], [currentAccount, preferences?.postInteractionSettings],
) )
+63 -106
View File
@@ -3,30 +3,17 @@ import {RichText} from '@atproto/api'
import {type SelfLabel} from '#/lib/moderation' import {type SelfLabel} from '#/lib/moderation'
import {logger} from '#/logger' import {logger} from '#/logger'
import {draftsStorage} from '#/state/drafts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {account, type ComposerDraft} from '#/storage' import {type ComposerDraft} from '#/storage'
import {type ComposerState} from './state/composer' import {type ComposerState} from './state/composer'
const AUTOSAVE_DELAY_MS = 1000 // 1 second debounce const AUTOSAVE_DELAY_MS = 1000 // 1 second debounce
const MAX_DRAFT_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
function generateDraftId(): string { function generateDraftId(): string {
return `draft-${Date.now()}-${Math.random().toString(36).substring(2, 9)}` return `draft-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
} }
function removeDraft(
allDrafts: Record<string, ComposerDraft>,
draftIdToRemove: string,
): Record<string, ComposerDraft> | null {
const result: Record<string, ComposerDraft> = {}
for (const key of Object.keys(allDrafts)) {
if (key !== draftIdToRemove) {
result[key] = allDrafts[key]
}
}
return Object.keys(result).length > 0 ? result : null
}
type SerializedImage = { type SerializedImage = {
alt: string alt: string
path: string path: string
@@ -257,45 +244,38 @@ export function useComposerDraft(
(state: ComposerState) => { (state: ComposerState) => {
if (!accountDid) return if (!accountDid) return
try { if (hasContent(state)) {
const allDrafts = account.get([accountDid, 'composerDrafts']) ?? {} const serialized = serializeDraft(state)
logger.debug('Draft serialized successfully', {
if (hasContent(state)) { draftId: currentDraftId,
const serialized = serializeDraft(state) hasPosts: serialized.thread.posts.length > 0,
logger.debug('Draft serialized successfully', { hasVideo: !!serialized.thread.posts[0]?.embed?.video,
draftId: currentDraftId,
hasPosts: serialized.thread.posts.length > 0,
hasVideo: !!serialized.thread.posts[0]?.embed?.video,
})
// Update the draft
account.set([accountDid, 'composerDrafts'], {
...allDrafts,
[currentDraftId]: serialized,
})
logger.info('Composer draft saved', {
draftId: currentDraftId,
textLength: state.thread.posts[0]?.richtext.text.length || 0,
})
} else {
// If no content, remove this draft
if (allDrafts[currentDraftId]) {
const remainingDrafts = removeDraft(allDrafts, currentDraftId)
if (remainingDrafts) {
account.set([accountDid, 'composerDrafts'], remainingDrafts)
} else {
account.remove([accountDid, 'composerDrafts'])
}
logger.debug('Empty draft removed', {draftId: currentDraftId})
}
}
} catch (e) {
logger.error('Failed to save composer draft', {
error: e,
message: e instanceof Error ? e.message : String(e),
stack: e instanceof Error ? e.stack : undefined,
}) })
draftsStorage.saveDraft(accountDid, currentDraftId, serialized).then(
() => {
logger.info('Composer draft saved', {
draftId: currentDraftId,
textLength: state.thread.posts[0]?.richtext.text.length || 0,
})
},
e => {
logger.error('Failed to save composer draft', {
error: e,
message: e instanceof Error ? e.message : String(e),
})
},
)
} else {
// If no content, remove this draft
draftsStorage.deleteDraft(accountDid, currentDraftId).then(
() => {
logger.debug('Empty draft removed', {draftId: currentDraftId})
},
e => {
logger.error('Failed to remove empty draft', {error: e})
},
)
} }
}, },
[accountDid, currentDraftId, hasContent], [accountDid, currentDraftId, hasContent],
@@ -335,69 +315,46 @@ export function useComposerDraft(
const loadDraft = useCallback((): Partial<ComposerState> | null => { const loadDraft = useCallback((): Partial<ComposerState> | null => {
if (!accountDid || !draftId) return null if (!accountDid || !draftId) return null
try { // Note: This is synchronous for now since MMKV is sync underneath.
const allDrafts = account.get([accountDid, 'composerDrafts']) // When migrating to a backend, this will need to become async.
if (!allDrafts) return null // For now, we call the async function but don't await it in this sync context.
// The actual loading happens in Composer.tsx via a useEffect.
let result: Partial<ComposerState> | null = null
const draft = allDrafts[draftId] // We need a sync return here for the current implementation
if (!draft) return null // This is a known limitation that will need refactoring when migrating to backend
draftsStorage.getDraft(accountDid, draftId).then(
// Check version compatibility draft => {
if (draft.version !== 1) { if (draft) {
logger.warn('Incompatible draft version, discarding', { logger.info('Composer draft loaded', {
version: draft.version, draftId,
}) textLength: draft.thread.posts[0]?.text.length || 0,
const remainingDrafts = removeDraft(allDrafts, draftId) })
if (remainingDrafts) { result = deserializeDraft(draft)
account.set([accountDid, 'composerDrafts'], remainingDrafts)
} else {
account.remove([accountDid, 'composerDrafts'])
} }
return null },
} e => {
logger.error('Failed to load composer draft', {error: e})
},
)
// Check if draft is too old // Since MMKV is actually sync, the promise resolves immediately
const age = Date.now() - draft.timestamp // This works for now but should be refactored for async backends
if (age > MAX_DRAFT_AGE_MS) { return result
logger.debug('Draft too old, discarding', {age})
const remainingDrafts = removeDraft(allDrafts, draftId)
if (remainingDrafts) {
account.set([accountDid, 'composerDrafts'], remainingDrafts)
} else {
account.remove([accountDid, 'composerDrafts'])
}
return null
}
logger.info('Composer draft loaded', {
draftId,
textLength: draft.thread.posts[0]?.text.length || 0,
})
return deserializeDraft(draft)
} catch (e) {
logger.error('Failed to load composer draft', {error: e})
return null
}
}, [accountDid, draftId]) }, [accountDid, draftId])
// Clear draft from storage // Clear draft from storage
const clearDraft = useCallback(() => { const clearDraft = useCallback(() => {
if (!accountDid) return if (!accountDid) return
try { draftsStorage.deleteDraft(accountDid, currentDraftId).then(
const allDrafts = account.get([accountDid, 'composerDrafts']) () => {
if (allDrafts && allDrafts[currentDraftId]) {
const remainingDrafts = removeDraft(allDrafts, currentDraftId)
if (remainingDrafts) {
account.set([accountDid, 'composerDrafts'], remainingDrafts)
} else {
account.remove([accountDid, 'composerDrafts'])
}
logger.debug('Composer draft cleared', {draftId: currentDraftId}) logger.debug('Composer draft cleared', {draftId: currentDraftId})
} },
} catch (e) { e => {
logger.error('Failed to clear composer draft', {error: e}) logger.error('Failed to clear composer draft', {error: e})
} },
)
}, [accountDid, currentDraftId]) }, [accountDid, currentDraftId])
// Auto-save on state changes (only for new drafts, not when editing existing ones) // Auto-save on state changes (only for new drafts, not when editing existing ones)
+33 -100
View File
@@ -1,15 +1,11 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useEffect, useState} from 'react'
import {logger} from '#/logger' import {type DraftItem, draftsStorage} from '#/state/drafts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {account, type ComposerDraft} from '#/storage' import {type ComposerDraft} from '#/storage'
const MAX_DRAFT_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days // Re-export DraftItem for consumers
export type {DraftItem}
export type DraftItem = {
id: string
draft: ComposerDraft
}
/** /**
* Hook for managing the drafts list. * Hook for managing the drafts list.
@@ -19,39 +15,18 @@ export function useDraftsList() {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const accountDid = currentAccount?.did const accountDid = currentAccount?.did
// State to force re-computation of drafts list after modifications // State for drafts list
const [refreshKey, setRefreshKey] = useState(0) const [drafts, setDrafts] = useState<DraftItem[]>([])
// Get all drafts, sorted by timestamp (newest first) // Load drafts on mount and when accountDid changes
const drafts = useMemo((): DraftItem[] => { useEffect(() => {
// refreshKey is used to trigger re-computation after deletions if (!accountDid) {
const _refresh = refreshKey setDrafts([])
if (!accountDid) return [] return
try {
const allDrafts = account.get([accountDid, 'composerDrafts'])
if (!allDrafts) return []
const now = Date.now()
const items: DraftItem[] = []
for (const [id, draft] of Object.entries(allDrafts)) {
// Skip invalid or too old drafts
if (draft.version !== 1) continue
if (now - draft.timestamp > MAX_DRAFT_AGE_MS) continue
items.push({id, draft})
}
// Sort by timestamp, newest first
items.sort((a, b) => b.draft.timestamp - a.draft.timestamp)
return items
} catch (e) {
logger.error('Failed to get drafts list', {error: e})
return []
} }
}, [accountDid, refreshKey])
draftsStorage.getAllDrafts(accountDid).then(setDrafts)
}, [accountDid])
const draftsCount = drafts.length const draftsCount = drafts.length
@@ -60,48 +35,19 @@ export function useDraftsList() {
(draftId: string) => { (draftId: string) => {
if (!accountDid) return if (!accountDid) return
try { draftsStorage.deleteDraft(accountDid, draftId).then(() => {
const allDrafts = account.get([accountDid, 'composerDrafts']) // Refresh the list after deletion
if (!allDrafts || !allDrafts[draftId]) return draftsStorage.getAllDrafts(accountDid).then(setDrafts)
})
const remainingDrafts: Record<string, ComposerDraft> = {}
for (const [id, draft] of Object.entries(allDrafts)) {
if (id !== draftId) {
remainingDrafts[id] = draft
}
}
if (Object.keys(remainingDrafts).length > 0) {
account.set([accountDid, 'composerDrafts'], remainingDrafts)
} else {
account.remove([accountDid, 'composerDrafts'])
}
// Trigger re-render to update the list
setRefreshKey(k => k + 1)
logger.debug('Draft deleted', {draftId})
} catch (e) {
logger.error('Failed to delete draft', {error: e, draftId})
}
}, },
[accountDid], [accountDid],
) )
// Get a specific draft by ID // Get a specific draft by ID
const getDraft = useCallback( const getDraft = useCallback(
(draftId: string): ComposerDraft | null => { async (draftId: string): Promise<ComposerDraft | null> => {
if (!accountDid) return null if (!accountDid) return null
return draftsStorage.getDraft(accountDid, draftId)
try {
const allDrafts = account.get([accountDid, 'composerDrafts'])
if (!allDrafts) return null
return allDrafts[draftId] ?? null
} catch (e) {
logger.error('Failed to get draft', {error: e, draftId})
return null
}
}, },
[accountDid], [accountDid],
) )
@@ -110,33 +56,19 @@ export function useDraftsList() {
const cleanupOldDrafts = useCallback(() => { const cleanupOldDrafts = useCallback(() => {
if (!accountDid) return if (!accountDid) return
try { draftsStorage.cleanupOldDrafts(accountDid).then(removedCount => {
const allDrafts = account.get([accountDid, 'composerDrafts'])
if (!allDrafts) return
const now = Date.now()
const remainingDrafts: Record<string, ComposerDraft> = {}
let removedCount = 0
for (const [id, draft] of Object.entries(allDrafts)) {
if (draft.version === 1 && now - draft.timestamp <= MAX_DRAFT_AGE_MS) {
remainingDrafts[id] = draft
} else {
removedCount++
}
}
if (removedCount > 0) { if (removedCount > 0) {
if (Object.keys(remainingDrafts).length > 0) { // Refresh the list after cleanup
account.set([accountDid, 'composerDrafts'], remainingDrafts) draftsStorage.getAllDrafts(accountDid).then(setDrafts)
} else {
account.remove([accountDid, 'composerDrafts'])
}
logger.debug('Cleaned up old drafts', {removedCount})
} }
} catch (e) { })
logger.error('Failed to cleanup old drafts', {error: e}) }, [accountDid])
}
// Refresh the drafts list (useful after saving a new draft)
const refreshDrafts = useCallback(() => {
if (!accountDid) return
draftsStorage.getAllDrafts(accountDid).then(setDrafts)
}, [accountDid]) }, [accountDid])
return { return {
@@ -145,5 +77,6 @@ export function useDraftsList() {
deleteDraft, deleteDraft,
getDraft, getDraft,
cleanupOldDrafts, cleanupOldDrafts,
refreshDrafts,
} }
} }