diff --git a/desktop/src/shared/ui/link-preview-attachment.tsx b/desktop/src/shared/ui/link-preview-attachment.tsx index 743d2471ef2..1fb449c9da3 100644 --- a/desktop/src/shared/ui/link-preview-attachment.tsx +++ b/desktop/src/shared/ui/link-preview-attachment.tsx @@ -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} /> ); diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 46bda44ffb7..b9dba110e45 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -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: { @@ -1390,6 +1398,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 @@ -10131,6 +10140,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__ = []; @@ -12691,7 +12709,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": diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index adacdb62952..39744136891 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -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("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( - "style defaults", - ) || - testInfo.title.includes( - "attachment-sized", + "draft auto-send", ) - ? 1_500 - : undefined, - linkPreviewMetadataStartBlockMs: - testInfo.title.includes( - "loading card before cold resolver work", - ) - ? 150 - : undefined, - } - : undefined; + ? 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: '', + 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, }) => { diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 50f792447c0..3bc6d4ae5d5 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -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. */