mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): enrich link previews with page metadata
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz> Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
This commit is contained in:
co-authored by
npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f
parent
d88313f369
commit
cb6fdffbb7
@@ -198,7 +198,7 @@ test("extractSupportedLinkPreviews skips markdown image link URLs", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("extractSupportedLinkPreviews requires bare URL boundaries", () => {
|
||||
test("extractSupportedLinkPreviews treats other absolute HTTPS URLs as generic", () => {
|
||||
assert.deepEqual(
|
||||
extractSupportedLinkPreviews(
|
||||
[
|
||||
@@ -207,7 +207,7 @@ test("extractSupportedLinkPreviews requires bare URL boundaries", () => {
|
||||
"(https://github.com/block/sprout/pull/2)",
|
||||
].join(" "),
|
||||
).map((preview) => preview.title),
|
||||
["block/sprout #2"],
|
||||
["evil-github.com", "example.com", "block/sprout #2"],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -252,3 +252,39 @@ test("isSupportedLinkAutolinkLabel matches normalized bare URL labels", () => {
|
||||
);
|
||||
assert.equal(isSupportedLinkAutolinkLabel("review this", preview), false);
|
||||
});
|
||||
|
||||
test("parseSupportedLinkPreview parses generic HTTPS URLs", () => {
|
||||
assert.deepEqual(
|
||||
parseSupportedLinkPreview("https://example.com/articles/rich-previews"),
|
||||
{
|
||||
kind: "generic-link",
|
||||
href: "https://example.com/articles/rich-previews",
|
||||
provider: "example.com",
|
||||
title: "example.com",
|
||||
typeLabel: "link",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("parseSupportedLinkPreview rejects generic HTTP URLs", () => {
|
||||
assert.equal(
|
||||
parseSupportedLinkPreview("http://example.com/articles/rich-previews"),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("extractSupportedLinkPreviews finds generic links and preserves exclusions", () => {
|
||||
assert.deepEqual(
|
||||
extractSupportedLinkPreviews(
|
||||
[
|
||||
"Read https://example.com/article first.",
|
||||
"`https://hidden.example.com/secret`",
|
||||
"then [the details](https://docs.example.org/details)",
|
||||
].join(" "),
|
||||
).map(({ kind, title }) => ({ kind, title })),
|
||||
[
|
||||
{ kind: "generic-link", title: "example.com" },
|
||||
{ kind: "generic-link", title: "the details" },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,18 +7,13 @@ export type SupportedLinkPreviewKind =
|
||||
| "google-drive-folder"
|
||||
| "google-docs-document"
|
||||
| "google-sheets-spreadsheet"
|
||||
| "google-slides-presentation";
|
||||
| "google-slides-presentation"
|
||||
| "generic-link";
|
||||
|
||||
export type SupportedLinkPreview = {
|
||||
kind: SupportedLinkPreviewKind;
|
||||
href: string;
|
||||
provider:
|
||||
| "GitHub"
|
||||
| "Linear"
|
||||
| "Google Drive"
|
||||
| "Google Docs"
|
||||
| "Google Sheets"
|
||||
| "Google Slides";
|
||||
provider: string;
|
||||
title: string;
|
||||
typeLabel:
|
||||
| "PR"
|
||||
@@ -28,13 +23,14 @@ export type SupportedLinkPreview = {
|
||||
| "folder"
|
||||
| "document"
|
||||
| "spreadsheet"
|
||||
| "presentation";
|
||||
| "presentation"
|
||||
| "link";
|
||||
};
|
||||
|
||||
const SUPPORTED_URL_RE =
|
||||
/(^|[\s([{<>"'])((?:https?:\/\/)?(?:(?:www\.)?github\.com|(?:www\.)?linear\.app|drive\.google\.com|docs\.google\.com)\/[^\s<>"'\]]+)/gi;
|
||||
/(^|[\s([{<>"'])(https:\/\/[^\s<>"'\]]+|(?:(?:www\.)?github\.com|(?:www\.)?linear\.app|drive\.google\.com|docs\.google\.com)\/[^\s<>"'\]]+)/gi;
|
||||
const MARKDOWN_SUPPORTED_LINK_RE =
|
||||
/!?\[([^\]\n]+)\]\(((?:https?:\/\/)?(?:(?:www\.)?github\.com|(?:www\.)?linear\.app|drive\.google\.com|docs\.google\.com)\/[^)\s<>"']+)\)/gi;
|
||||
/!?\[([^\]\n]+)\]\((https:\/\/[^)\s<>"']+|(?:(?:www\.)?github\.com|(?:www\.)?linear\.app|drive\.google\.com|docs\.google\.com)\/[^)\s<>"']+)\)/gi;
|
||||
const MAX_PREVIEWS = 8;
|
||||
|
||||
type HiddenRange = {
|
||||
@@ -431,12 +427,27 @@ export function parseSupportedLinkPreview(
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
const recognized =
|
||||
parseGithubLink(parsed) ??
|
||||
parseLinearIssue(parsed) ??
|
||||
parseGoogleDriveLink(parsed) ??
|
||||
parseGoogleDocsLink(parsed)
|
||||
);
|
||||
parseGoogleDocsLink(parsed);
|
||||
if (recognized) return recognized;
|
||||
const hostname = normalizeHostname(parsed);
|
||||
if (
|
||||
parsed.protocol !== "https:" ||
|
||||
[
|
||||
"github.com",
|
||||
"linear.app",
|
||||
"drive.google.com",
|
||||
"docs.google.com",
|
||||
].includes(hostname)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = hostname;
|
||||
return createPreview("generic-link", parsed, provider, "link", provider);
|
||||
}
|
||||
|
||||
export function isSupportedLinkAutolinkLabel(
|
||||
|
||||
@@ -4,61 +4,64 @@ import { invokeTauri } from "@/shared/api/tauri";
|
||||
|
||||
import type { SupportedLinkPreview } from "./linkPreview";
|
||||
|
||||
const GOOGLE_FALLBACK_TITLES = new Set([
|
||||
"Drive file",
|
||||
"Drive folder",
|
||||
"Document",
|
||||
"Spreadsheet",
|
||||
"Presentation",
|
||||
]);
|
||||
type LinkPreviewMetadata = {
|
||||
title: string;
|
||||
siteName: string | null;
|
||||
};
|
||||
|
||||
const titleCache = new Map<string, Promise<string | null> | string | null>();
|
||||
const metadataCache = new Map<
|
||||
string,
|
||||
Promise<LinkPreviewMetadata | null> | LinkPreviewMetadata | null
|
||||
>();
|
||||
|
||||
function fetchLinkPreviewTitle(href: string): Promise<string | null> {
|
||||
return invokeTauri<string | null>("fetch_link_preview_title", { href });
|
||||
/** Clear ephemeral metadata when the active relay/community changes. */
|
||||
export function resetLinkPreviewMetadataCache(): void {
|
||||
metadataCache.clear();
|
||||
}
|
||||
|
||||
function shouldResolveTitle(preview: SupportedLinkPreview): boolean {
|
||||
return (
|
||||
preview.kind.startsWith("google-") &&
|
||||
GOOGLE_FALLBACK_TITLES.has(preview.title)
|
||||
function fetchLinkPreviewMetadata(
|
||||
href: string,
|
||||
): Promise<LinkPreviewMetadata | null> {
|
||||
return invokeTauri<LinkPreviewMetadata | null>(
|
||||
"fetch_link_preview_metadata",
|
||||
{
|
||||
href,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function cacheTitle(href: string): Promise<string | null> {
|
||||
const cached = titleCache.get(href);
|
||||
function cacheMetadata(href: string): Promise<LinkPreviewMetadata | null> {
|
||||
const cached = metadataCache.get(href);
|
||||
if (cached instanceof Promise) return cached;
|
||||
if (cached !== undefined) return Promise.resolve(cached);
|
||||
|
||||
const promise = fetchLinkPreviewTitle(href)
|
||||
.then((title) => {
|
||||
titleCache.set(href, title);
|
||||
return title;
|
||||
const promise = fetchLinkPreviewMetadata(href)
|
||||
.then((metadata) => {
|
||||
metadataCache.set(href, metadata);
|
||||
return metadata;
|
||||
})
|
||||
.catch(() => {
|
||||
titleCache.set(href, null);
|
||||
metadataCache.set(href, null);
|
||||
return null;
|
||||
});
|
||||
titleCache.set(href, promise);
|
||||
metadataCache.set(href, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function useResolvedLinkPreviews(
|
||||
previews: SupportedLinkPreview[],
|
||||
): SupportedLinkPreview[] {
|
||||
const [resolvedTitles, setResolvedTitles] = React.useState<
|
||||
Record<string, string>
|
||||
const [resolvedMetadata, setResolvedMetadata] = React.useState<
|
||||
Record<string, LinkPreviewMetadata>
|
||||
>({});
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const pending = previews.filter(shouldResolveTitle);
|
||||
if (pending.length === 0) return undefined;
|
||||
|
||||
for (const preview of pending) {
|
||||
const cached = titleCache.get(preview.href);
|
||||
if (typeof cached === "string" && cached) {
|
||||
setResolvedTitles((current) =>
|
||||
for (const preview of previews) {
|
||||
const cached = metadataCache.get(preview.href);
|
||||
if (cached && !(cached instanceof Promise)) {
|
||||
setResolvedMetadata((current) =>
|
||||
current[preview.href] === cached
|
||||
? current
|
||||
: { ...current, [preview.href]: cached },
|
||||
@@ -66,12 +69,12 @@ export function useResolvedLinkPreviews(
|
||||
continue;
|
||||
}
|
||||
|
||||
void cacheTitle(preview.href).then((title) => {
|
||||
if (cancelled || !title) return;
|
||||
setResolvedTitles((current) =>
|
||||
current[preview.href] === title
|
||||
void cacheMetadata(preview.href).then((metadata) => {
|
||||
if (cancelled || !metadata) return;
|
||||
setResolvedMetadata((current) =>
|
||||
current[preview.href] === metadata
|
||||
? current
|
||||
: { ...current, [preview.href]: title },
|
||||
: { ...current, [preview.href]: metadata },
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -84,9 +87,17 @@ export function useResolvedLinkPreviews(
|
||||
return React.useMemo(
|
||||
() =>
|
||||
previews.map((preview) => {
|
||||
const title = resolvedTitles[preview.href];
|
||||
return title ? { ...preview, title } : preview;
|
||||
const metadata = resolvedMetadata[preview.href];
|
||||
if (!metadata) return preview;
|
||||
return {
|
||||
...preview,
|
||||
title: metadata.title,
|
||||
provider:
|
||||
preview.kind === "generic-link" && metadata.siteName
|
||||
? metadata.siteName
|
||||
: preview.provider,
|
||||
};
|
||||
}),
|
||||
[previews, resolvedTitles],
|
||||
[previews, resolvedMetadata],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ function LinkPreviewLogo({ preview }: { preview: SupportedLinkPreview }) {
|
||||
return <GoogleSheetsLogo className="h-4 w-4" />;
|
||||
case "google-slides-presentation":
|
||||
return <GoogleSlidesLogo className="h-4 w-4" />;
|
||||
case "generic-link":
|
||||
return <ExternalLink aria-hidden="true" className="h-4 w-4" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user