feat(uploads) add image annotation tools to composer uploads#1488
Open
tellaho wants to merge 8 commits into
Open
feat(uploads) add image annotation tools to composer uploads#1488tellaho wants to merge 8 commits into
tellaho wants to merge 8 commits into
Conversation
Add a freehand annotation mode to composer image attachments: a pencil button in the attachment lightbox enters canvas mode, saving composites the strokes into a PNG at natural resolution and re-uploads it as an in-place replacement, and a revert button restores the pre-edit original — both without closing the dialog. - ComposerImageEditor.tsx (new): canvas overlay editor with 6 pen colors, 3 stroke widths, undo (button + Cmd+Z), clear, cancel, and save. Strokes are stored in natural-image coordinates so the on-screen preview matches the exported PNG exactly; export loads the image with crossOrigin="anonymous" plus a ?cors=1 cache-buster so year-long immutable cache entries from before the CORS fix can't poison the CORS check - useMediaUpload.ts: add uploadEditedAttachment (uploads annotated bytes, swaps the descriptor into the same slot, disables send while in flight), revertAttachment, and an originalsByUrl map that remembers pre-edit originals; chained edits keep the earliest original and entries prune automatically when attachments leave the composer - ComposerAttachments.tsx: extract MediaAttachmentItem with view/edit modes, pencil + revert + close toolbar in the lightbox, and Escape-in-edit-mode exiting canvas mode instead of closing the dialog; items are keyed by original URL so the URL swap on save/revert doesn't remount (and close) the open dialog - media_proxy.rs: send Access-Control-Allow-Origin: * on proxied media responses — WKWebView omits the Origin header on CORS-mode image GETs while still enforcing the response header, so echoing the request origin can never work; the origin gate still 403s foreign origins first, and now also allows loopback dev origins (http://localhost:<port>) since tauri dev serves the webview from a per-worktree localhost port, with unit tests for the gate - useAttachmentEditing.ts (new) + MessageComposer.tsx: wire save/revert and migrate spoiler marking to the replacement URL so an edited spoilered image stays spoilered; ForumComposerMediaStatus.tsx wires the same for forum composers - composer-image-draw.spec.ts (new, registered in the smoke project): covers draw → save → in-place replace, Escape behavior, revert without dialog close, and spoiler survival across an edit Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
… bar - Move Cancel/Save CTAs from the floating pen toolbar to a fixed bar in the top-right corner of the lightbox overlay, using the shared Button component (ghost variant for Cancel, default for Save) - Move the drawing controls (width slider, color swatches, undo) into the same top bar, sliding in leftward from the CTAs via animate-in/slide-in-from-right on mount - Strip the pill styling from the toolbar (background, padding, blur, separators) so controls sit directly on the overlay - Replace the three Thin/Medium/Thick preset buttons with a compact range slider: 4-12px in 2px steps (five whole-pixel stops, default 6px), slim custom track and thumb via ::-webkit-slider-thumb - Color swatches now double as stroke-width previews: each fixed 20px button renders its color as an inner dot sized to the current width, animating as the slider moves; selection ring stays fixed-size - Ring outline only on the black swatch (for contrast on the dark overlay), drawn outside the dot so it never overlaps the color - Remove the clear-all (trashcan) control - Cancel covers discarding - and the hover scale effect on swatches Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…image
- Add redo: stroke history is now a single {strokes, undone} state
object so undo/redo move strokes between stacks atomically
(StrictMode-safe); new strokes clear the redo stack per editor
convention. Redo button (Redo2 icon) next to Undo, plus Shift+Cmd+Z
handling in the existing Cmd+Z keyboard listener
- Replace the crosshair cursor with a dynamic brush preview: the
native cursor is hidden over the canvas (cursor-none) and a
pointer-following DOM dot renders the active color at the exact
on-screen stroke width, with a faint white ring for visibility on
light image areas. Positioned imperatively via ref during
pointermove (no per-move re-renders); fades out on pointer leave.
A DOM element sized in CSS pixels is guaranteed accurate, unlike a
cursor: url(...) image
- Make the lightbox image fully unselectable: pointer-events-none on
the <img> alongside the existing select-none and draggable={false}
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…save - ComposerAttachments: replace the undo-arrow icon button in the attachment lightbox view mode with the shared Button (default variant) labeled "Revert"; keeps the composer-attachment-revert test id and the "Revert to original" tooltip, drops the unused Undo2 import - ComposerImageEditor: disable the Cancel CTA while a save is in flight so the editor cannot be dismissed mid-upload Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
The loopback media proxy's origin check accepted any http(s)
localhost/127.0.0.1 origin on any port, and the proxy is spawned
unconditionally (lib.rs) — so in packaged builds, any local web page
that discovered the proxy port could make JS-readable, CORS-approved
requests to relay media. The widening was never needed: the composer
canvas crossOrigin="anonymous" reload and <img> loads use the no-Origin
path, which the unconditional ACAO:* already covers.
- desktop/src-tauri/src/media_proxy.rs:
- is_webview_origin now accepts only the packaged custom-scheme
origins (tauri://localhost, http://tauri.localhost), plus the one
exact dev origin gated behind cfg!(debug_assertions)
- The dev origin is derived from the merged Tauri config's
build.devUrl at proxy spawn (stored in ProxyState), so per-worktree
dev ports from scripts/instance-env.sh keep working — no hardcoded
localhost:1420
- ACAO:* stays unconditional (required for no-Origin CORS image
loads; foreign pages are rejected by the origin gate first)
- Tests updated: exact-dev-origin-only in debug builds, dev origin
rejected in release builds, remote/lookalike origins still rejected
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…ob churn - ComposerAttachments.tsx: handleEditorSave now closes the lightbox dialog after a successful annotated-image upload instead of returning to view mode. Each save uploads a fresh blob to the relay while revert/re-edit only drops the client reference, so keeping the modal open made rapid save/redraw cycles (and their orphaned blobs) frictionless — closing it adds deliberate friction per reviewer feedback - ComposerAttachments.tsx: update MediaAttachmentItem doc comment — save now closes the dialog while revert still keeps it open (parent keys the item by original URL so the URL swap doesn't remount it) - ComposerImageEditor.tsx: update handleSave comment to reflect that the parent closes the lightbox on success - composer-image-draw.spec.ts: main test now asserts the dialog closes on save and the annotated thumbnail appears in the composer, then reopens the lightbox to exercise the revert flow (revert behavior unchanged); spoiler test drops the now-unneeded Escape press after save Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…y changes Replace the image editor's CORS-based canvas export with a Tauri IPC byte fetch + same-origin blob: URL, eliminating the need for any media proxy CORS headers or origin-gate changes. media_proxy.rs is restored byte-for-byte to its pre-branch state (228122f), removing the CORS surface the security review flagged entirely instead of narrowing it. - desktop/src-tauri/src/media_proxy.rs: full revert — no ACAO headers, no is_webview_origin, original strict origin gate restored - desktop/src-tauri/src/commands/media_download.rs: new fetch_media_bytes command reusing the existing download plumbing — validate_download_url SSRF check (relay origin + /media/ path only), 50 MiB cap via fetch_blob_bytes, detect_and_validate_mime content policy; registered in lib.rs - ComposerImageEditor.tsx: renderAnnotatedPng fetches original bytes over IPC, wraps them in a typed Blob, and decodes from a blob: URL (revoked in finally) — blob URLs are same-origin so the canvas stays un-tainted without crossOrigin="anonymous" or the ?cors=1 cache-buster; takes new sourceUrl/sourceType props since the Rust command validates the raw relay URL, not the proxy-rewritten one - ComposerAttachments.tsx: pass attachment.url and attachment.type to the editor alongside the proxy-rewritten display src - tauriMedia.ts (new): fetchMediaBytes wrapper returning Uint8Array<ArrayBuffer> — its own module because tauri.ts is at the check-file-sizes.mjs line ceiling - e2eBridge.ts: mock fetch_media_bytes with an in-page fetch (specs serve the URL via page.route) - composer-image-draw.spec.ts: route comment updated — the ACAO header on the mock route now exists only for the bridge's in-page fetch, not the production path Trade-offs: the export load skips WKWebView's HTTP cache (one extra relay round-trip per save), and the avatar snapshot fetch in selfProfileStorage.ts returns to its pre-branch silent-null fallback. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho
added a commit
that referenced
this pull request
Jul 3, 2026
Move the text spoiler toggle into the expanded formatting toolbar and make it text-only — it no longer mirrors onto pending media attachments. Media spoilers are now toggled per image/video via a new control in the attachment lightbox's top-right cluster, next to the close button and backed by the existing per-URL spoilered set. The lightbox toggle is placed left of where the upcoming image edit/draw control (PR #1488) will sit, with a note to hide it while edit mode is active once that feature lands. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho
added a commit
that referenced
this pull request
Jul 3, 2026
Move the text spoiler toggle into the expanded formatting toolbar and make it text-only — it no longer mirrors onto pending media attachments. Media spoilers are now toggled per image/video via a new control in the attachment lightbox's top-right cluster, next to the close button and backed by the existing per-URL spoilered set. The lightbox toggle is placed left of where the upcoming image edit/draw control (PR #1488) will sit, with a note to hide it while edit mode is active once that feature lands. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Keep this PR's MediaAttachmentItem as the composer attachment render path:
annotation needs view/edit modes, ComposerImageEditor, revert, and the
key={originalUrl ?? attachment.url} guard so edit/revert URL swaps don't
remount and close the open dialog. Drop main's now-unused
AttachmentMediaLightbox (and its SimpleImageLightbox import) from this file
so we don't ship two lightboxes for the same thumbnails; the shared
SimpleImageLightbox component itself is untouched and still used by
ViewImageToolPreview. Trivial import hunks take the union.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Screen.Recording.2026-07-02.at.11.19.20.PM.mov
Category: new-feature
User Impact: Users can now draw on uploaded images before sending them from the desktop composer.
Problem: Uploaded images could only be previewed or removed, so any quick markup required leaving Buzz, editing elsewhere, and re-uploading. That made lightweight annotation feel heavier than the message it was supporting.
Solution: This adds an in-composer image editor with draw, undo/redo, save, and revert flows, while preserving originals across drafts, spoiler toggles, sends, and cancels. Export now fetches the source media bytes over Tauri IPC and wraps them in a same-origin blob URL, so canvas editing works without broadening the media proxy's CORS/origin policy.
File changes
desktop/playwright.config.ts
Adds the project configuration needed for the new image-editor browser coverage.
desktop/src-tauri/src/commands/media_download.rs
Adds a validated
fetch_media_bytescommand for the composer image editor. It reuses the same relay URL validation, size cap, MIME checks, and Rust-side fetch path as media downloads so canvas export can avoid webview CORS entirely.desktop/src-tauri/src/lib.rs
Registers the new media-byte fetch command with Tauri so the desktop webview can request source image bytes for editor export.
desktop/src/features/forum/ui/ForumComposerMediaStatus.tsx
Routes forum image attachments through the shared edited-upload path so replacement uploads report progress consistently with the message composer.
desktop/src/features/messages/lib/useAttachmentEditing.ts
Introduces the composer glue for saving edited attachments, reverting to originals, and migrating spoiler state across URL swaps.
desktop/src/features/messages/lib/useMediaUpload.ts
Adds the edited-attachment upload path and in-memory original/revert bookkeeping while preserving attachment order. It also prunes edit state when attachments leave the composer through send, remove, cancel, or draft changes.
desktop/src/features/messages/ui/ComposerAttachments.tsx
Extends the attachment lightbox with draw entry points, save/revert controls, dialog behavior, and image/video-specific affordances.
desktop/src/features/messages/ui/ComposerImageEditor.tsx
Adds the canvas drawing experience, including brush colors and width, brush preview, undo/redo, keyboard shortcuts, save disabled states, and PNG export from source bytes.
desktop/src/features/messages/ui/MessageComposer.tsx
Connects message send, cancel, and draft transitions to the edited-attachment lifecycle so temporary edit state is cleared or restored at the right time.
desktop/src/shared/api/tauriMedia.ts
Adds the frontend wrapper for fetching relay media bytes over Tauri IPC and returning them as a
Uint8Arrayfor same-origin blob export.desktop/src/shared/hooks/useWebviewScrollBoundaryLock.ts
Refines scroll-boundary locking for modal/lightbox interactions so drawing and overscroll behavior do not fight each other.
desktop/src/shared/styles/globals/theme.css
Removes global scrollbar styling that conflicted with the updated modal and scroll-boundary behavior.
desktop/src/testing/e2eBridge.ts
Adds an E2E mock for
fetch_media_bytesso image-editor tests can exercise the IPC export path in browser automation.desktop/tests/e2e/composer-image-draw.spec.ts
Adds end-to-end coverage for drawing, saving, upload replacement, reverting, Escape behavior, and spoiler URL migration.
desktop/tests/e2e/overscroll-boundary.spec.ts
Updates overscroll-boundary coverage to match the shared hook behavior after the lightbox/editor changes.
Reproduction Steps
Screenshots/Demos
Draft PR — demo or screen recording to be added before marking ready.