From c12012a83a5b02a315e42be23dec3e471c8d87af Mon Sep 17 00:00:00 2001 From: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:28:16 -0400 Subject: [PATCH] Add link and video right-click Download/Copy menus (bug-bash media lane) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buzz renders inside a native webview whose default context menu has no useful link or media actions, so right-clicking a link was a no-op (#02ef9f1b) and videos had no Download at all. This adds custom in-app context menus for links and inline videos, and dedupes the existing image menu into a single shared primitive. - New `MediaContextMenu` primitive (portal menu + deferred-dismiss hook), reused three ways: image (Copy/Download), link (Open/Copy), video (Download/Copy). Each variant emits a distinct data attribute (`data-image-context-menu` / `data-link-context-menu` / `data-video-context-menu`) plus the shared `data-media-context-menu` marker, so e2e locators target one surface without aliasing another; the image variant additionally carries `data-image-lightbox-controls` for the in-lightbox dismiss guard. - Links: `ExternalLinkAnchor` adds Open link (OS opener) / Copy link. - Videos: the `useVideoContextMenu` hook owns the inline right-click menu's state and actions — Download video (reuses the existing `download_file` command) and Copy link — keeping that logic out of the large `VideoPlayer` and out of the pure `videoDownload.ts` helpers. Render classification and download eligibility are independent (pure, DOM-free helpers in `markdown/mediaEntry.ts`): - `isVideoMedia` classifies from the imeta MIME (`video/*`), authoritative when present, with a legacy path-extension fallback for pre-MIME events. Previously only `.mp4` was recognized, so relay `.webm`/`.mov` fell into the image renderer and Tyler's video Download was absent. The relay-media proxy regex is widened to `mp4|webm|mov` to match. - `isRelayDownloadable` gates Download on relay `/media/` origin, not mere URL presence, so an external video renders and offers Copy link but omits Download (which the Rust SSRF gate would reject anyway). The original relay `src` is the download URL, never the rewritten proxy `resolvedSrc`. Download eligibility is reactive and fails closed. The relay origin resolves asynchronously and is commonly still null on first render; a synchronous read would freeze eligibility, so `mediaUrl.ts` exposes the origin as an observable (`subscribeRelayOrigin` + `getCachedRelayOrigin`) consumed via `useRelayOrigin()` (`useSyncExternalStore`). `MarkdownVideoPlayer` reads it, so eligibility recomputes when the origin resolves and on workspace switch. `isRelayDownloadable` now returns false for an unresolved origin (fail closed) rather than optimistically true — before, an off-relay `/media/` URL wrongly showed Download until resolution. A generation token (`beginRelayOriginFetch`) ensures an in-flight origin fetch from a previous community can never publish after a workspace switch. The origin fetch is retried inside the port poll loop (a single fire-and-forget attempt could fail before the Tauri bridge was ready and leave the origin null forever), and each poll invoke is bounded by the remaining deadline (`withDeadline`) so a never-settling IPC call cannot hang startup past the 5s budget. Redirect-hop SSRF: authenticated media *download* fetches use a dedicated no-redirect client built by `build_media_fetch_client() -> Result` (`redirect::Policy::none()`). `build_app_state()` calls `.expect(...)` so a build failure panics loudly at startup rather than silently degrading to a redirect-following client (fail closed, not fail open). A relay 3xx is returned verbatim and mapped by `redirect_refusal_error` to an actionable "redirect refused" error — not a generic relay failure and never a followed cross-origin fetch of the minted media Authorization header. The app-wide `http_client` is unchanged. Follow-up (NOT fixed here — this PR hardens the download path only, and must not be read as making rendering redirect-safe): the media *rendering* path shares the same exposure. The axum proxy in `src-tauri/src/media_proxy.rs` and the `buzz-media://` protocol handler (registered in `src-tauri/src/lib.rs`) both mint a media Authorization header and follow redirects, so a relay 3xx to a private/off-origin target would forward that header cross-origin. It is left untouched here because it is the passive streaming path (a legitimate relay CDN-offload redirect would break `