The previous approach replaced both / and : with _, but the reverse
transformation couldn't distinguish between them. This caused cache
misses for paths containing both characters.
Use encodeURIComponent/decodeURIComponent for a proper reversible
encoding.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplifies web IndexedDB storage by using idb-keyval instead of the
full idb library. This reduces bundle size and aligns with the pattern
used in src/storage/archive/db/index.web.ts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Platform-specific files (.web.ts, .native.ts) are resolved by the
bundler automatically - just import normally, don't use require()
- Paginated APIs should use useInfiniteQuery, not useQuery
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
When loading a GIF from a draft, the URL was being corrupted with
double query strings like:
`?ww=498&hh=498?hh=498&ww=498`
This happened because:
1. serializeGif() adds ?ww=X&hh=Y&alt=Z to the Tenor URL
2. parseGifFromUrl() returned the full URL including our params
3. resolveGif() in resolve.ts then appends MORE params via string
concatenation, creating a second ?
Fix: Strip our custom params (ww, hh, alt) from the URL in
parseGifFromUrl() before returning it, so the reconstructed GIF
has a clean base URL.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix NaN aspectRatio when rehydrating GIFs from drafts by:
1. Adding validation in parseTenorGif to reject invalid dimensions
(NaN, zero, or negative values)
2. Adding defensive checks in GifEmbed to fallback to 1:1 aspect
ratio if dimensions are invalid
3. Adding defensive checks in composer ExternalEmbedGif to fallback
to 16:9 if gif.media_formats.gif.dims is missing or invalid
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace manual error object inspection with the proper
AppBskyDraftCreateDraft.DraftLimitReachedError type check for
cleaner and more reliable error handling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pb_xl padding to the drafts list content container for better
visual spacing at the bottom of the list.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When editing an existing draft (vs creating a new one), use "Save
changes" instead of "Save draft" in the save/discard prompts. This
provides clearer context to the user about what action they're taking.
Add isEditingDraft prop to DraftsButton and ComposerTopBar, and
update both prompts (in DraftsButton and Composer) with conditional
copy based on whether we're editing an existing draft.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix draft preview images not showing on web and add proper aspect
ratio support:
1. Try to load all images regardless of the exists cache flag, which
may be stale due to async cache population timing
2. Use Image.loadAsync() from expo-image to get image dimensions
3. Pass dimensions to viewImages for proper aspect ratio in previews
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix "Cannot read property 'url' of undefined" error when rehydrating
drafts with GIFs. The Gif object was missing required properties like
url, content_description, and media_formats.preview that are needed
by useResolveGifQuery and other components.
Also preserve alt text through serialization by storing it in URL
query params alongside dimensions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
On iOS (and web), the media cache wasn't populated before the drafts
query ran, causing drafts with local media to incorrectly show as
"missing media" on app restart. The issue would resolve itself after
closing and reopening the composer because by then the cache was ready.
This fix adds ensureMediaCachePopulated() and awaits it in useDrafts
before checking which media exists locally.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Switch from the generic logger to the new drafts-specific logger
for better log categorization.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a Drafts context to the logger system for better log categorization
and debugging of draft-related operations.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use the new object-based expo-file-system API (SDK 54+) with Directory
and File classes instead of the legacy function-based API. The new API
provides synchronous operations for file/directory existence checks,
creation, copying, deletion, and listing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace local-only draft storage with the new `app.bsky.draft.*` server API:
- getDrafts, createDraft, updateDraft, deleteDraft endpoints
Key changes:
- Add api.ts with type converters (ComposerState <-> server Draft)
- Update hooks.ts to use server API instead of local storage
- Simplify storage.ts/storage.web.ts for local media caching only
- Media stored locally via localRef pattern (filepath in server draft)
- GIFs stored as external embeds with Tenor URL + dimensions
- Hide drafts button when replying (reply drafts not supported)
- Show "different device" note when media is missing locally
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When re-saving a draft, the code was trying to copy media files that
were already in drafts storage to new locations, causing copy errors.
Changes:
- Add extractLocalIdFromPath() to detect if a path is already in drafts
- Track loadedMediaMap in ComposerState for identifying reusable media
- Only delete old media that wasn't reused during re-save
- Pass loadedMediaMap when saving to enable media reuse detection
- Disable pointer events on draft media preview
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Only show the save/discard prompt when pressing the Drafts button if
the composer has unsaved changes (isDirty). If the content is unchanged
from a loaded draft or was just saved, go directly to the drafts list.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add mark_saved action that resets isDirty to false and updates the
draftId. This is dispatched after successfully saving a draft, allowing
the user to close the composer without a discard prompt since their
changes have been saved.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Render header outside FlatList on native, inside on web
- Use web() helper for conditional web-only props
- Replace ItemSeparatorComponent with mt_lg margin on items
- Add minHeight on web for better dialog sizing
- Simplify header structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace custom image preview with AutoSizedImage for single images
and ImageLayoutGrid for multiple images. This gives drafts the same
polished image display as regular posts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>