mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): render compact link preview images
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
co-authored by
Taylor Ho
parent
814d1eee27
commit
008a1d599a
@@ -0,0 +1,45 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { resolveLinkPreview } from "./useResolvedLinkPreviews.ts";
|
||||
|
||||
const preview = {
|
||||
kind: "generic-link",
|
||||
href: "https://example.com/story",
|
||||
provider: "example.com",
|
||||
title: "example.com/story",
|
||||
typeLabel: "link",
|
||||
};
|
||||
|
||||
test("pending metadata reserves the image treatment", () => {
|
||||
assert.deepEqual(resolveLinkPreview(preview, undefined), {
|
||||
...preview,
|
||||
imageState: "pending",
|
||||
});
|
||||
});
|
||||
|
||||
test("resolved image metadata keeps the reserved image treatment", () => {
|
||||
const resolved = resolveLinkPreview(preview, {
|
||||
title: "A story",
|
||||
siteName: "Example",
|
||||
imageDataUrl: "data:image/jpeg;base64,abc",
|
||||
imageDomain: "cdn.example.com",
|
||||
});
|
||||
|
||||
assert.equal(resolved.imageState, "image");
|
||||
assert.equal(resolved.provider, "Example");
|
||||
assert.equal(resolved.imageDomain, "cdn.example.com");
|
||||
});
|
||||
|
||||
test("resolved metadata without a complete image collapses to the compact treatment", () => {
|
||||
const resolved = resolveLinkPreview(preview, {
|
||||
title: "A story",
|
||||
siteName: "Example",
|
||||
imageDataUrl: null,
|
||||
imageDomain: null,
|
||||
});
|
||||
|
||||
assert.equal(resolved.imageState, "none");
|
||||
assert.equal(resolved.imageDataUrl, null);
|
||||
assert.equal(resolved.imageDomain, null);
|
||||
});
|
||||
@@ -50,12 +50,47 @@ function cacheMetadata(href: string): Promise<LinkPreviewMetadata | null> {
|
||||
return promise;
|
||||
}
|
||||
|
||||
export type LinkPreviewImageState = "pending" | "image" | "none";
|
||||
|
||||
export type ResolvedLinkPreview = SupportedLinkPreview & {
|
||||
imageState: LinkPreviewImageState;
|
||||
};
|
||||
|
||||
type ResolvedMetadataByHref = Record<
|
||||
string,
|
||||
LinkPreviewMetadata | null | undefined
|
||||
>;
|
||||
|
||||
export function resolveLinkPreview(
|
||||
preview: SupportedLinkPreview,
|
||||
metadata: LinkPreviewMetadata | null | undefined,
|
||||
): ResolvedLinkPreview {
|
||||
if (metadata === undefined) {
|
||||
return { ...preview, imageState: "pending" };
|
||||
}
|
||||
if (metadata === null) {
|
||||
return { ...preview, imageState: "none" };
|
||||
}
|
||||
|
||||
const hasImage = Boolean(metadata.imageDataUrl && metadata.imageDomain);
|
||||
return {
|
||||
...preview,
|
||||
title: metadata.title,
|
||||
provider:
|
||||
preview.kind === "generic-link" && metadata.siteName
|
||||
? metadata.siteName
|
||||
: preview.provider,
|
||||
imageDataUrl: hasImage ? metadata.imageDataUrl : null,
|
||||
imageDomain: hasImage ? metadata.imageDomain : null,
|
||||
imageState: hasImage ? "image" : "none",
|
||||
};
|
||||
}
|
||||
|
||||
export function useResolvedLinkPreviews(
|
||||
previews: SupportedLinkPreview[],
|
||||
): SupportedLinkPreview[] {
|
||||
const [resolvedMetadata, setResolvedMetadata] = React.useState<
|
||||
Record<string, LinkPreviewMetadata>
|
||||
>({});
|
||||
): ResolvedLinkPreview[] {
|
||||
const [resolvedMetadata, setResolvedMetadata] =
|
||||
React.useState<ResolvedMetadataByHref>({});
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -72,7 +107,7 @@ export function useResolvedLinkPreviews(
|
||||
}
|
||||
|
||||
void cacheMetadata(preview.href).then((metadata) => {
|
||||
if (cancelled || !metadata) return;
|
||||
if (cancelled) return;
|
||||
setResolvedMetadata((current) =>
|
||||
current[preview.href] === metadata
|
||||
? current
|
||||
@@ -88,20 +123,9 @@ export function useResolvedLinkPreviews(
|
||||
|
||||
return React.useMemo(
|
||||
() =>
|
||||
previews.map((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,
|
||||
imageDataUrl: metadata.imageDataUrl,
|
||||
imageDomain: metadata.imageDomain,
|
||||
};
|
||||
}),
|
||||
previews.map((preview) =>
|
||||
resolveLinkPreview(preview, resolvedMetadata[preview.href]),
|
||||
),
|
||||
[previews, resolvedMetadata],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Globe, X } from "lucide-react";
|
||||
|
||||
import type { SupportedLinkPreview } from "@/shared/lib/linkPreview";
|
||||
import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -89,7 +89,7 @@ function GoogleSlidesLogo({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function LinkPreviewLogo({ preview }: { preview: SupportedLinkPreview }) {
|
||||
function LinkPreviewLogo({ preview }: { preview: ResolvedLinkPreview }) {
|
||||
switch (preview.kind) {
|
||||
case "github-issue":
|
||||
case "github-pull-request":
|
||||
@@ -118,32 +118,45 @@ export function LinkPreviewAttachment({
|
||||
}: {
|
||||
className?: string;
|
||||
onRemove?: () => void;
|
||||
preview: SupportedLinkPreview;
|
||||
preview: ResolvedLinkPreview;
|
||||
}) {
|
||||
const showImage = Boolean(preview.imageDataUrl && preview.imageDomain);
|
||||
const reserveImage = preview.imageState !== "none";
|
||||
const showImage = preview.imageState === "image";
|
||||
|
||||
return (
|
||||
<div className={cn("relative w-80 max-w-full shrink-0", className)}>
|
||||
<Attachment
|
||||
className="w-full no-underline shadow-none"
|
||||
data-image-state={preview.imageState}
|
||||
data-link-preview={preview.kind}
|
||||
orientation={showImage ? "vertical" : "horizontal"}
|
||||
orientation="horizontal"
|
||||
>
|
||||
{showImage ? (
|
||||
<div className="relative -mx-3 -mt-2.5 w-[calc(100%+1.5rem)] overflow-hidden border-b border-border/70 bg-muted">
|
||||
<img
|
||||
alt={`Preview from ${preview.imageDomain}`}
|
||||
className="aspect-[1.91/1] w-full object-cover"
|
||||
src={preview.imageDataUrl ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{!showImage ? (
|
||||
{reserveImage ? (
|
||||
<AttachmentMedia
|
||||
aria-hidden={showImage ? undefined : "true"}
|
||||
className="h-12 w-16 rounded-lg bg-muted sm:h-14 sm:w-[6.6875rem]"
|
||||
data-link-preview-thumbnail=""
|
||||
variant="image"
|
||||
>
|
||||
{showImage ? (
|
||||
<img
|
||||
alt={`Preview from ${preview.imageDomain}`}
|
||||
className="h-full w-full object-cover"
|
||||
src={preview.imageDataUrl ?? undefined}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="h-full w-full animate-pulse bg-muted-foreground/10"
|
||||
data-link-preview-skeleton=""
|
||||
/>
|
||||
)}
|
||||
</AttachmentMedia>
|
||||
) : (
|
||||
<AttachmentMedia className="link-preview-media">
|
||||
<LinkPreviewLogo preview={preview} />
|
||||
</AttachmentMedia>
|
||||
) : null}
|
||||
<AttachmentContent className={showImage ? "w-full" : undefined}>
|
||||
)}
|
||||
<AttachmentContent>
|
||||
<div className="truncate text-xs font-medium leading-4 text-muted-foreground">
|
||||
{preview.provider}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user