mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
**Category:** fix **User Impact:** Buzz pull request, issue, and repository links now show compact, useful metadata cards in received messages, including messages sent by agents and the CLI. **Problem:** Sender-authored snapshots protect recipients from external preview fetches, but that change also removed recipient-side cards for trusted Buzz entity links when the sender did not attach snapshots. **Solution:** Resolve recognized Buzz entities only against the active relay and show signed repository identity, title, and compact builder context with the current inline Buzz mark in the favicon slot, but without avatars, thumbnails, or external image fetches. Entity metadata wins over conflicting sender snapshots, while unsupported or unavailable metadata retains a safe text fallback. <details> <summary>File changes</summary> **desktop/playwright.config.ts** Adds the entity-link regression spec to the smoke test project. **desktop/src/features/messages/ui/useComposerLinkPreviews.tsx** Treats recognized Buzz entity cards as complete without generating snapshot tags and retains fallback cards when relay metadata is absent. **desktop/src/shared/lib/useResolvedLinkPreviews.test.mjs** Covers kind-scoped entity detection, trusted relay metadata, root-scoped lifecycle queries, exact single-repository root binding, image-less pending state, and fallback behavior. **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Resolves signed repository, pull request, and issue metadata from the active relay. Entity roots fail closed unless they carry exactly one matching repository tag; lifecycle queries are root-scoped before limits; successful metadata remains stable until relay/community reset, and PR commit context uses the immutable root event rather than an unindexed update query. **desktop/src/shared/ui/compact-link-preview-attachment.tsx** Uses Buzz repository identity as the compact card provider and avoids reserving thumbnail space for image-less entity cards. **desktop/src/shared/ui/markdown.tsx** Routes message cards through the combined entity/snapshot preview hook. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.test.mjs** Proves relay-authenticated entity metadata beats a forged sender snapshot while preserving mixed-link content order. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.ts** Combines recipient-resolved Buzz entities with sender-authored external snapshots using explicit trust precedence and first-seen ordering. **desktop/tests/e2e/entity-link-recipient-cards.spec.ts** Exercises repository identity, PR workflow context, repository metadata, image-less rendering, and composer send behavior for agent/CLI-style entity links. </details> ## Reproduction steps 1. Open a channel containing a message sent without `link-preview` tags whose content includes valid `buzz://pr`, `buzz://issue`, or `buzz://repo` links. 2. Confirm each card shows its repository identity and signed title; PRs/issues also show compact lifecycle context, and repositories show description/status/default branch. 3. Confirm the cards use the Buzz mark in the favicon slot with no avatar, thumbnail, or reserved image area. 4. Compose and send a message containing a Buzz entity link; confirm sending is not blocked waiting for a snapshot. 5. Send a message containing both a Buzz entity link and a snapshot-backed HTTPS link; confirm cards follow content order and the HTTPS link remains sender-snapshot-only. ## Screenshots ### Recipient view — Buzz-branded metadata cards Repository identity, title, and compact builder context render with the current inline Buzz mark in the favicon slot and no avatar, thumbnail, or reserved image space.  ## Validation At commit `7bc70b0a9f70392bd062ed25b1d2362cc4021a40` with a clean working tree: - Pre-push hooks passed: branch skew, desktop check, desktop typecheck, and full desktop unit suite - Full desktop unit suite: 4,560 passed - Purpose-built Playwright regression after a fresh E2E build: 2 passed - Screenshot regenerated from the same commit and visually inspected Originating conversation: Buzz channel `c2859932-b679-4091-9c7e-f5a65deddd64`, thread `93c3e7be59a8d1ec10b4992efd783a2a79f253a10f10d39746c6ad41b0d5bb42`. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import * as React from "react";
|
|
|
|
import {
|
|
extractSupportedLinkPreviews,
|
|
type SupportedLinkPreview,
|
|
} from "@/shared/lib/linkPreview";
|
|
import { parseLinkPreviewSnapshots } from "@/shared/lib/linkPreviewSnapshot";
|
|
import {
|
|
isBuzzEntityPreview,
|
|
type ResolvedLinkPreview,
|
|
useResolvedLinkPreviews,
|
|
withEntityFallbacks,
|
|
} from "@/shared/lib/useResolvedLinkPreviews";
|
|
|
|
/**
|
|
* Resolve the link-preview cards for a rendered message.
|
|
*
|
|
* External URLs render exclusively from sender-authored
|
|
* `["link-preview","snapshot",…]` tags (`parseLinkPreviewSnapshots`) — the
|
|
* privacy model shipped in the rich-link-previews work: recipients never
|
|
* contact external sites.
|
|
*
|
|
* `buzz://pr|issue|repo` entity links (and relay clone URLs, which normalize
|
|
* onto `buzz://repo`) are the exception and are rendered recipient-side:
|
|
* their metadata comes from the community relay itself via the entity metadata
|
|
* loader, so the sender-snapshot privacy model does not apply — and senders
|
|
* (CLI, agents, mobile) attach no snapshot tags for them. Recognized entity
|
|
* cards win
|
|
* over conflicting sender snapshots, and cards retain their first-seen order in
|
|
* the message.
|
|
*/
|
|
export function mergeMessageLinkPreviews(
|
|
candidates: SupportedLinkPreview[],
|
|
snapshots: ResolvedLinkPreview[],
|
|
entities: ResolvedLinkPreview[],
|
|
): ResolvedLinkPreview[] {
|
|
const snapshotsByHref = new Map(
|
|
snapshots.map((preview) => [preview.href, preview]),
|
|
);
|
|
const entitiesByHref = new Map(
|
|
entities.map((preview) => [preview.href, preview]),
|
|
);
|
|
|
|
return candidates.flatMap((candidate) => {
|
|
const preview = isBuzzEntityPreview(candidate)
|
|
? entitiesByHref.get(candidate.href)
|
|
: snapshotsByHref.get(candidate.href);
|
|
return preview ? [preview] : [];
|
|
});
|
|
}
|
|
|
|
export function useMessageLinkPreviews({
|
|
content,
|
|
interactive,
|
|
linkPreviewTags,
|
|
linkPreviewsSuppressed,
|
|
relayOrigin,
|
|
}: {
|
|
content: string;
|
|
interactive: boolean;
|
|
linkPreviewTags?: readonly (readonly string[])[];
|
|
linkPreviewsSuppressed: boolean;
|
|
relayOrigin: string | null;
|
|
}): ResolvedLinkPreview[] {
|
|
const linkPreviewCandidates = React.useMemo(
|
|
() =>
|
|
interactive && !linkPreviewsSuppressed
|
|
? extractSupportedLinkPreviews(content, relayOrigin)
|
|
: [],
|
|
[content, interactive, linkPreviewsSuppressed, relayOrigin],
|
|
);
|
|
const entityLinkPreviews = React.useMemo(
|
|
() => linkPreviewCandidates.filter(isBuzzEntityPreview),
|
|
[linkPreviewCandidates],
|
|
);
|
|
const relayResolvedEntityLinkPreviews =
|
|
useResolvedLinkPreviews(entityLinkPreviews);
|
|
const resolvedEntityLinkPreviews = React.useMemo(
|
|
() =>
|
|
withEntityFallbacks(entityLinkPreviews, relayResolvedEntityLinkPreviews),
|
|
[entityLinkPreviews, relayResolvedEntityLinkPreviews],
|
|
);
|
|
return React.useMemo(() => {
|
|
const snapshots =
|
|
interactive && !linkPreviewsSuppressed
|
|
? parseLinkPreviewSnapshots(linkPreviewTags, content, relayOrigin)
|
|
: [];
|
|
return mergeMessageLinkPreviews(
|
|
linkPreviewCandidates,
|
|
snapshots,
|
|
resolvedEntityLinkPreviews,
|
|
);
|
|
}, [
|
|
content,
|
|
interactive,
|
|
linkPreviewCandidates,
|
|
linkPreviewTags,
|
|
linkPreviewsSuppressed,
|
|
relayOrigin,
|
|
resolvedEntityLinkPreviews,
|
|
]);
|
|
}
|