mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +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>
57 lines
1.5 KiB
TypeScript
57 lines
1.5 KiB
TypeScript
import * as React from "react";
|
|
|
|
/** User preference for how link previews are presented. */
|
|
export type LinkPreviewStyle = "compact" | "rich";
|
|
|
|
export const LINK_PREVIEW_STYLE_STORAGE_KEY =
|
|
"buzz.appearance.linkPreviewStyle";
|
|
export const DEFAULT_LINK_PREVIEW_STYLE: LinkPreviewStyle = "compact";
|
|
|
|
const listeners = new Set<() => void>();
|
|
let linkPreviewStyle = readStoredLinkPreviewStyle();
|
|
|
|
export function parseLinkPreviewStyle(
|
|
value: string | null | undefined,
|
|
): LinkPreviewStyle {
|
|
return value === "rich" || value === "compact"
|
|
? value
|
|
: DEFAULT_LINK_PREVIEW_STYLE;
|
|
}
|
|
|
|
function readStoredLinkPreviewStyle(): LinkPreviewStyle {
|
|
try {
|
|
return parseLinkPreviewStyle(
|
|
globalThis.localStorage?.getItem(LINK_PREVIEW_STYLE_STORAGE_KEY),
|
|
);
|
|
} catch {
|
|
return DEFAULT_LINK_PREVIEW_STYLE;
|
|
}
|
|
}
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
export function getLinkPreviewStyle(): LinkPreviewStyle {
|
|
return linkPreviewStyle;
|
|
}
|
|
|
|
export function setLinkPreviewStyle(style: LinkPreviewStyle): void {
|
|
linkPreviewStyle = style;
|
|
try {
|
|
globalThis.localStorage?.setItem(LINK_PREVIEW_STYLE_STORAGE_KEY, style);
|
|
} catch {
|
|
// Persistence is best-effort; the in-memory preference still applies.
|
|
}
|
|
for (const listener of listeners) listener();
|
|
}
|
|
|
|
export function useLinkPreviewStyle(): LinkPreviewStyle {
|
|
return React.useSyncExternalStore(
|
|
subscribe,
|
|
getLinkPreviewStyle,
|
|
() => DEFAULT_LINK_PREVIEW_STYLE,
|
|
);
|
|
}
|