mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
## Overview **Category:** improvement **User impact:** Link previews appear in the composer and travel as privacy-safe sender-authored snapshots, so recipients never contact the linked site merely by opening a conversation. **Problem:** Cold-cache link paste could freeze the composer before the URL painted; recipient-side unfurling leaked visits; invalid or unresolved preview work could interfere with sending or leave dead cards behind. **Solution:** Paint pasted links before starting cold resolver work, resolve only in the sender's composer, attach only complete validated snapshots at Send, and render authored snapshots without recipient fallback fetching. ## Behavior - **Cold paste stays responsive:** bare and angle-bracket URL paste paths commit the visible link before resolver work begins. - **Sender-only fetching:** metadata is resolved while composing; recipients render only the sender-authored snapshot. - **Send never waits:** pending, failed, invalid, and unsendable previews are omitted. They do not block or cancel the message. - **Terminal misses disappear:** failed, timed-out, or 404 resolver results remove the composer card while preserving visible link text. - **Display-text links work:** Markdown links such as `[review the pull request](…)` produce and send the same snapshots as bare URLs. - **Compact and Rich presentation:** Compact remains the default; Rich preserves source description line breaks and paragraphs. - **Immediate draft-wide dismissal:** clicking × immediately hides all previews for the draft, suppresses links pasted later, and emits only `["link-preview", "none"]`. No confirmation detour. Suppression resets after send or clearing the draft. - **Zero recipient fallback:** missing, stale, malformed, off-relay, unsupported, or suppressed snapshots remain ordinary visible links; recipients never regenerate them. ## Implementation - Resolve previews from deferred composer URL state so paste can paint first. - Upload finished preview media to the active community relay and snapshot only valid, sendable media references. - Atomically capture ready snapshots at submit time; never append a late preview after send. - Validate snapshot and suppression tags in desktop/native and relay ingestion, rejecting duplicate or mixed forms. - Render composer previews as stable 55px attachment cards at desktop and narrow widths. - Add deterministic E2E coverage for cold paste, ready/pending/failed/invalid previews, display-text links, multiline Rich descriptions, immediate dismissal, later-pasted links, and suppression reset. ## Validation Validated head: `9807ba8952f190e76153834abf8ab61dd40be5e2` - Push hooks passed: `check-push-org`, branch skew, desktop check, mobile tests, desktop tests, Rust tests, and desktop Tauri checks. - Focused screenshot E2E at the validated head: 5/5 passed across Compact/Rich composer and recipient states, 800px/420px geometry, display-text links, multiline descriptions, and immediate dismissal. - PR CI was triggered for this exact head and is currently running; completed checks are green at the time of this update. - Worktree is clean and both PR head and validated branch resolve to `9807ba895…`. ## Screenshots ### Compact composer | Loading | Ready | |---|---| |  |  | ### Rich composer | Loading | Ready | |---|---| |  |  | ### Responsive composer | 800px loading | 800px ready | |---|---| |  |  | | 420px loading | 420px ready | |---|---| |  |  | ### Recipient presentation | Compact | Rich | |---|---| |  |  | ### Display-text Markdown link | Composer | Recipient | |---|---| |  |  | ### Rich multiline description  ### Immediate dismissal | Before × | Immediately after × | |---|---| |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
117 lines
3.3 KiB
JavaScript
117 lines
3.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildLinkPreviewSnapshotTag,
|
|
parseLinkPreviewSnapshots,
|
|
} from "./linkPreviewSnapshot.ts";
|
|
|
|
const HASH = "a".repeat(64);
|
|
const ORIGIN = "https://relay.example";
|
|
const CONTENT = "Read https://linear.app/acme/issue/ABC-123/example";
|
|
const URL = "https://linear.app/acme/issue/ABC-123/example";
|
|
const valid = [
|
|
"link-preview",
|
|
"snapshot",
|
|
"1",
|
|
URL,
|
|
"Example",
|
|
"Linear",
|
|
"Description",
|
|
`${ORIGIN}/media/${HASH}.png`,
|
|
HASH,
|
|
"",
|
|
"",
|
|
];
|
|
|
|
test("authored snapshots render only exact local media tied to message content", () => {
|
|
assert.equal(parseLinkPreviewSnapshots([valid], CONTENT, ORIGIN).length, 1);
|
|
assert.equal(
|
|
parseLinkPreviewSnapshots([valid], "no matching link", ORIGIN).length,
|
|
0,
|
|
);
|
|
assert.equal(parseLinkPreviewSnapshots([valid], CONTENT, null).length, 0);
|
|
});
|
|
|
|
test("authored snapshots reject remote, malformed, credentialed, and hash-mismatched media", () => {
|
|
for (const image of [
|
|
`https://evil.example/media/${HASH}.png`,
|
|
`${ORIGIN}/media/${HASH}.png?token=leak`,
|
|
`${ORIGIN}/media/${HASH}.png#fragment`,
|
|
`https://user@relay.example/media/${HASH}.png`,
|
|
`${ORIGIN}/media/${HASH}.svg`,
|
|
`${ORIGIN}/media/${HASH}.png/extra`,
|
|
]) {
|
|
const tag = [...valid];
|
|
tag[7] = image;
|
|
assert.equal(
|
|
parseLinkPreviewSnapshots([tag], CONTENT, ORIGIN).length,
|
|
0,
|
|
image,
|
|
);
|
|
}
|
|
const mismatch = [...valid];
|
|
mismatch[8] = "b".repeat(64);
|
|
assert.equal(
|
|
parseLinkPreviewSnapshots([mismatch], CONTENT, ORIGIN).length,
|
|
0,
|
|
);
|
|
});
|
|
|
|
test("snapshot tags sanitize control characters and enforce UTF-8 byte limits", () => {
|
|
const tag = buildLinkPreviewSnapshotTag({
|
|
canonicalUrl: URL,
|
|
title: `Title\n${"😀".repeat(100)}`,
|
|
siteName: `Linear\u0000${"é".repeat(100)}`,
|
|
description: `First\nSecond\t${"😀".repeat(300)}`,
|
|
imageUrl: "",
|
|
imageSha256: "",
|
|
faviconUrl: "",
|
|
faviconSha256: "",
|
|
});
|
|
|
|
assert.ok(tag);
|
|
assert.equal(tag[6].startsWith("First\nSecond"), true);
|
|
assert.equal(tag[6].includes("\t"), false);
|
|
for (const value of tag.slice(4, 6)) {
|
|
assert.ok(
|
|
Array.from(value).every((char) => {
|
|
const code = char.charCodeAt(0);
|
|
return code > 0x1f && code !== 0x7f;
|
|
}),
|
|
);
|
|
}
|
|
assert.ok(Buffer.byteLength(tag[4], "utf8") <= 300);
|
|
assert.ok(Buffer.byteLength(tag[5], "utf8") <= 100);
|
|
assert.ok(Buffer.byteLength(tag[6], "utf8") <= 1000);
|
|
});
|
|
|
|
test("snapshot tags omit canonical URLs rejected by native validation", () => {
|
|
for (const canonicalUrl of [
|
|
`${URL}#`,
|
|
`${URL}#details`,
|
|
`http://linear.app/acme/issue/ABC-123/example`,
|
|
`https://user@linear.app/acme/issue/ABC-123/example`,
|
|
]) {
|
|
assert.equal(
|
|
buildLinkPreviewSnapshotTag({
|
|
canonicalUrl,
|
|
title: "Example",
|
|
siteName: "Linear",
|
|
description: "Description",
|
|
imageUrl: "",
|
|
imageSha256: "",
|
|
faviconUrl: "",
|
|
faviconSha256: "",
|
|
}),
|
|
null,
|
|
canonicalUrl,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("messages without authored snapshots never create recipient previews", () => {
|
|
assert.deepEqual(parseLinkPreviewSnapshots([], CONTENT, ORIGIN), []);
|
|
assert.deepEqual(parseLinkPreviewSnapshots(undefined, CONTENT, ORIGIN), []);
|
|
});
|