From daad7533ef057d73ff54397c1311f6ff3a2fa163 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Thu, 2 Jul 2026 20:38:15 -0700 Subject: [PATCH 1/8] feat(desktop): draw on uploaded images in the composer lightbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:) 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 Signed-off-by: Taylor Ho --- desktop/playwright.config.ts | 1 + desktop/src-tauri/src/media_proxy.rs | 69 +++- .../forum/ui/ForumComposerMediaStatus.tsx | 15 + .../messages/lib/useAttachmentEditing.ts | 53 +++ .../features/messages/lib/useMediaUpload.ts | 118 ++++++ .../messages/ui/ComposerAttachments.tsx | 380 +++++++++++------ .../messages/ui/ComposerImageEditor.tsx | 383 ++++++++++++++++++ .../features/messages/ui/MessageComposer.tsx | 11 + desktop/tests/e2e/composer-image-draw.spec.ts | 179 ++++++++ 9 files changed, 1088 insertions(+), 121 deletions(-) create mode 100644 desktop/src/features/messages/lib/useAttachmentEditing.ts create mode 100644 desktop/src/features/messages/ui/ComposerImageEditor.tsx create mode 100644 desktop/tests/e2e/composer-image-draw.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 3b5f35d99..1c21176f0 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ "**/observer-feed-screenshots.spec.ts", "**/file-attachment.spec.ts", "**/image-attachment-gallery.spec.ts", + "**/composer-image-draw.spec.ts", "**/video-attachment.spec.ts", "**/spoiler.spec.ts", "**/mentions.spec.ts", diff --git a/desktop/src-tauri/src/media_proxy.rs b/desktop/src-tauri/src/media_proxy.rs index 10a02d947..e25fc9a19 100644 --- a/desktop/src-tauri/src/media_proxy.rs +++ b/desktop/src-tauri/src/media_proxy.rs @@ -25,6 +25,26 @@ struct ProxyState { app_handle: tauri::AppHandle, } +/// True when `origin` belongs to the Tauri webview itself: the custom-scheme +/// origins of packaged builds, or a loopback origin used by `tauri dev` +/// (`devUrl` is `http://localhost:`). Loopback origins +/// imply code already running on this machine, which could reach the proxy +/// directly anyway — allowing them adds no new exposure, while remote +/// websites keep getting 403. +fn is_webview_origin(origin: &str) -> bool { + if origin == "tauri://localhost" || origin == "http://tauri.localhost" { + return true; + } + let Some(rest) = origin + .strip_prefix("http://") + .or_else(|| origin.strip_prefix("https://")) + else { + return false; + }; + let host = rest.split(':').next().unwrap_or(rest); + host == "localhost" || host == "127.0.0.1" +} + async fn proxy_handler(AxumState(state): AxumState, req: Request) -> Response { // Allow requests with no Origin (e.g.