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
@@ -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}
/>
);
+19 -1
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":
+112 -30
View File
@@ -280,44 +280,63 @@ test.beforeEach(async ({ page }, testInfo) => {
"link-preview-image",
],
}
: testInfo.title.includes("link preview") ||
testInfo.title.includes("supported Compact")
: testInfo.title.includes(
"sent link preview media uses",
)
? {
mediaProxyInitiallyUnavailable: true,
linkPreviewMetadata: {
title: "Buzz pull request",
siteName: "GitHub",
description:
"A sender-authored preview snapshot.",
imageDataUrl: null,
imageDomain: null,
imageDataUrl:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
imageDomain: "opengraph.githubassets.com",
faviconDataUrl:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
},
linkPreviewMetadataDelayMs:
testInfo.title.includes(
"loading card before cold resolver work",
)
? 10_000
: testInfo.title.includes(
"send does not wait",
)
? 3_000
: testInfo.title.includes("draft auto-send")
? 500
: testInfo.title.includes(
"style defaults",
) ||
testInfo.title.includes(
"attachment-sized",
)
? 1_500
: undefined,
linkPreviewMetadataStartBlockMs:
testInfo.title.includes(
"loading card before cold resolver work",
)
? 150
: undefined,
}
: undefined;
: testInfo.title.includes("link preview") ||
testInfo.title.includes("supported Compact")
? {
linkPreviewMetadata: {
title: "Buzz pull request",
siteName: "GitHub",
description:
"A sender-authored preview snapshot.",
imageDataUrl: null,
imageDomain: null,
},
linkPreviewMetadataDelayMs:
testInfo.title.includes(
"loading card before cold resolver work",
)
? 10_000
: testInfo.title.includes(
"send does not wait",
)
? 3_000
: testInfo.title.includes(
"draft auto-send",
)
? 500
: testInfo.title.includes(
"style defaults",
) ||
testInfo.title.includes(
"attachment-sized",
)
? 1_500
: undefined,
linkPreviewMetadataStartBlockMs:
testInfo.title.includes(
"loading card before cold resolver work",
)
? 150
: undefined,
}
: undefined;
const mock = testInfo.title.includes("unresolvable preview")
? { linkPreviewMetadata: null, linkPreviewMetadataDelayMs: 800 }
: baseMock;
@@ -462,6 +481,69 @@ test("markdown tables overflow wide content and fill the message when narrow", a
.toBeLessThanOrEqual(1);
});
test("sent link preview media uses the authenticated proxy in compact and rich cards", async ({
page,
}) => {
const previewUrl = "https://github.com/block/buzz/pull/3246?proxy=1";
const fallbackMediaPattern =
/^buzz-media:\/\/localhost\/media\/[\da-f]{64}\.png$/;
const proxyMediaPattern =
/^http:\/\/127\.0\.0\.1:54321\/media\/[\da-f]{64}\.png$/;
await page.route("http://127.0.0.1:54321/media/**", (route) =>
route.fulfill({
body: '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20"><rect width="40" height="20" fill="#22c55e"/></svg>',
contentType: "image/svg+xml",
}),
);
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.getByTestId("message-input").fill(previewUrl);
await waitForReadyComposerSnapshots(page);
await page.getByTestId("send-message").click();
const row = page.getByTestId("message-row").last();
const compactPreview = row.locator(
'[data-link-preview="github-pull-request"]',
);
const compactThumbnail = compactPreview
.locator("[data-link-preview-thumbnail] img")
.first();
const compactFavicon = compactPreview.locator(
"img[data-link-preview-hostname-favicon]",
);
await expect(compactThumbnail).toHaveAttribute("src", fallbackMediaPattern);
await expect(compactFavicon).toHaveAttribute("src", fallbackMediaPattern);
const releasedPort = await page.evaluate(() =>
window.__BUZZ_E2E_RELEASE_MEDIA_PROXY__?.(),
);
expect(releasedPort).toBe(54321);
await expect(compactThumbnail).toHaveAttribute("src", proxyMediaPattern);
await expect(compactFavicon).toHaveAttribute("src", proxyMediaPattern);
await expect
.poll(() => compactThumbnail.evaluate((image) => image.naturalWidth))
.toBe(40);
await openSettings(page, "appearance");
await page.getByTestId("link-preview-style-trigger").click();
await page.getByTestId("link-preview-style-rich").click();
await page.getByTestId("settings-back-to-app").click();
const richPreview = row.locator(
'[data-link-preview="github-pull-request"][data-link-preview-inline]',
);
const richThumbnail = richPreview
.locator("[data-link-preview-thumbnail] img")
.first();
const richFavicon = richPreview.locator("img[data-link-preview-favicon]");
await expect(richThumbnail).toHaveAttribute("src", proxyMediaPattern);
await expect(richFavicon).toHaveAttribute("src", proxyMediaPattern);
await expect
.poll(() => richThumbnail.evaluate((image) => image.naturalWidth))
.toBe(40);
});
test("link preview style defaults to compact and Rich unfurls descriptions", async ({
page,
}) => {
+2
View File
@@ -275,6 +275,8 @@ type MockBridgeOptions = {
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. */