mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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. | |  |  | Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user