fix(link-previews): proxy sent preview media (#5627)

## Overview

**Category:** fix
**User Impact:** Sent link previews now reliably display their thumbnail
and favicon when the media is hosted on the relay.

**Problem:** Sent preview cards loaded relay-hosted snapshot media
directly, so authenticated relay requests could fail even though the
snapshot itself was valid. **Solution:** Rewrite snapshot media at the
shared card render boundary through Buzz's authenticated local media
proxy, preserving the original display domain and rerendering when the
proxy becomes ready.

## Changes

<details>
<summary>File changes</summary>

**desktop/src/shared/ui/link-preview-attachment.tsx**
Routes sent preview thumbnails and favicons through authenticated relay
media handling above the Compact/Rich fork while preserving original
metadata.

**desktop/src/testing/e2eBridge.ts**
Adds an opt-in proxy-readiness seam that deterministically re-arms the
production media lookup when released.

**desktop/tests/e2e/messaging.spec.ts**
Covers the real send, snapshot, recipient, and card-render path for
Compact and Rich previews, including fallback URLs, proxied URLs, and
decoded image content.

**desktop/tests/helpers/bridge.ts**
Exposes the opt-in media-proxy startup state to E2E tests.

</details>

## Reproduction Steps

1. Send a link whose preview snapshot includes a relay-hosted thumbnail
and favicon.
2. Inspect the sent message card in Compact mode and confirm both images
render after the local media proxy becomes ready.
3. Switch link previews to Rich mode and confirm the thumbnail and
favicon continue to render.
4. Run the focused Playwright regression:
`pnpm exec playwright test tests/e2e/messaging.spec.ts --project=smoke
--grep "sent link preview media uses the authenticated proxy"`


## Before / After

| Before | After |
| --- | --- |
| Relay-hosted preview media fails to load. | The sent preview thumbnail
and favicon render through the authenticated media proxy. |
| ![Before: sent link preview with a missing
thumbnail](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5627/link-preview-before.png)
| ![After: sent link preview with the thumbnail
rendered](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5627/link-preview-after.png)
|

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-12 10:24:21 -07:00
committed by GitHub
parent 8a2c9af2db
commit 884ed8a5d3
4 changed files with 147 additions and 33 deletions

View File

@@ -1,5 +1,7 @@
import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews";
import { useLinkPreviewStyle } from "@/shared/lib/linkPreviewStylePreference";
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { useMediaProxyPort } from "@/shared/lib/useMediaProxyPort";
import { CompactLinkPreviewAttachment } from "@/shared/ui/compact-link-preview-attachment";
import {
type LinkPreviewImageLightboxComponent,
@@ -21,6 +23,16 @@ export function LinkPreviewAttachment({
preview: ResolvedLinkPreview;
showControls?: boolean;
}) {
useMediaProxyPort();
const renderedPreview = {
...preview,
faviconDataUrl: preview.faviconDataUrl
? rewriteRelayUrl(preview.faviconDataUrl)
: null,
imageDataUrl: preview.imageDataUrl
? rewriteRelayUrl(preview.imageDataUrl)
: null,
};
const style = useLinkPreviewStyle();
if (style === "rich") {
return (
@@ -29,7 +41,7 @@ export function LinkPreviewAttachment({
ImageLightbox={ImageLightbox}
onOpen={onOpen}
onRemove={onRemove}
preview={preview}
preview={renderedPreview}
showControls={showControls}
/>
);
@@ -40,7 +52,7 @@ export function LinkPreviewAttachment({
className={className}
onOpen={onOpen}
onRemove={onRemove}
preview={preview}
preview={renderedPreview}
showControls={showControls}
/>
);

View File

@@ -61,6 +61,10 @@ import type {
RawInstallRuntimeResult,
RuntimeFileConfigSubset,
} from "@/shared/api/tauri";
import {
ensureRelayOriginFetch,
resetMediaCaches,
} from "@/shared/lib/mediaUrl";
import { normalizePubkey } from "@/shared/lib/pubkey";
import {
isValidLinkPreviewSnapshotCanonicalUrl,
@@ -318,6 +322,8 @@ type E2eConfig = {
applyCommunityDelayMs?: number;
openDmDelayMs?: number;
sendMessageDelayMs?: number;
/** Hold the media proxy at port 0 until the E2E release seam is invoked. */
mediaProxyInitiallyUnavailable?: boolean;
/** Hold mock send live echoes until the E2E release seam is invoked. */
deferSendMessageLiveEcho?: boolean;
/** Close the first channel-window live REQ; its retry is accepted. */
@@ -1101,6 +1107,8 @@ declare global {
command: string;
payload: unknown;
}>;
/** Release a mock media proxy held at port 0 and return its ready port. */
__BUZZ_E2E_RELEASE_MEDIA_PROXY__?: () => number;
/** Release mock send events that were stored but withheld from live subscribers. */
__BUZZ_E2E_RELEASE_SEND_MESSAGE_LIVE_ECHO__?: () => number;
__BUZZ_E2E_EMIT_MEDIA_UPLOAD_PHASE__?: (input: {
@@ -1391,6 +1399,7 @@ const CHANNEL_WINDOW_AUX_DELETION_KINDS = new Set([
// in e2e (instead of the `buzz-media://` fallback). The reaction guard
// asserts against this exact port.
const MOCK_MEDIA_PROXY_PORT = 54321;
let mockMediaProxyPort = MOCK_MEDIA_PROXY_PORT;
// A relay-hosted custom emoji used by the reaction guard. Its URL matches
// `rewriteRelayUrl()`'s `/media/{64-hex}.{ext}` pattern on the relay origin, so
@@ -10132,6 +10141,15 @@ export function maybeInstallE2eTauriMocks() {
window.__BUZZ_E2E_COMMANDS__ = [];
window.__BUZZ_E2E_COMMAND_PAYLOADS__ = [];
window.__BUZZ_E2E_COMMAND_LOG__ = [];
mockMediaProxyPort = config.mock?.mediaProxyInitiallyUnavailable
? 0
: MOCK_MEDIA_PROXY_PORT;
window.__BUZZ_E2E_RELEASE_MEDIA_PROXY__ = () => {
mockMediaProxyPort = MOCK_MEDIA_PROXY_PORT;
resetMediaCaches();
ensureRelayOriginFetch();
return mockMediaProxyPort;
};
window.__BUZZ_E2E_EMIT_MOCK_HUDDLE_TTS_SPEAKER__ = (payload) =>
emit("huddle-tts-speaker-level", payload);
window.__BUZZ_E2E_SIGNED_EVENTS__ = [];
@@ -12696,7 +12714,7 @@ export function maybeInstallE2eTauriMocks() {
activeConfig,
);
case "get_media_proxy_port":
return MOCK_MEDIA_PROXY_PORT;
return mockMediaProxyPort;
case "pick_and_upload_media":
return await resolveMockUploadDescriptors(activeConfig);
case "pick_and_upload_image":