mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(previews): replace remove-previews text link with inline X on preview card
- link-preview-attachment.tsx: add optional onRemove prop that renders a small X button floating just outside the card's top-right corner (left-full top-0, z-20 above the link trigger), fading in on message row hover via group-hover/message and reachable by keyboard via focus-visible - link-preview-attachment.tsx: wrap the card in a relative container carrying the width classes (w-80 max-w-full shrink-0) since the card's overflow-hidden would clip a button positioned outside its bounds - link-preview-attachment.tsx: remove the AttachmentActions block with the hover-revealed ExternalLink icon, eliminating the reserved flex gap on card hover - markdown.tsx: drop the "Remove previews for everyone" text button below the preview list and its spacing wrapper; pass onRemove to each LinkPreviewAttachment instead, opening the same confirmation dialog - removal flow and AlertDialog confirmation are unchanged; X only renders when onRemoveLinkPreviewsForEveryone is available (own messages) Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import { ExternalLink, Globe } from "lucide-react";
|
||||
import { Globe, X } from "lucide-react";
|
||||
|
||||
import type { SupportedLinkPreview } from "@/shared/lib/linkPreview";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentActions,
|
||||
AttachmentContent,
|
||||
AttachmentMedia,
|
||||
AttachmentTitle,
|
||||
@@ -113,65 +112,71 @@ function LinkPreviewLogo({ preview }: { preview: SupportedLinkPreview }) {
|
||||
|
||||
export function LinkPreviewAttachment({
|
||||
className,
|
||||
onRemove,
|
||||
preview,
|
||||
}: {
|
||||
className?: string;
|
||||
onRemove?: () => void;
|
||||
preview: SupportedLinkPreview;
|
||||
}) {
|
||||
const showImage = Boolean(preview.imageDataUrl && preview.imageDomain);
|
||||
|
||||
return (
|
||||
<Attachment
|
||||
className={cn(
|
||||
"w-80 max-w-full shrink-0 no-underline shadow-none",
|
||||
className,
|
||||
)}
|
||||
data-link-preview={preview.kind}
|
||||
orientation={showImage ? "vertical" : "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={`Automatic preview from ${preview.imageDomain}`}
|
||||
className="aspect-[1.91/1] w-full object-cover"
|
||||
src={preview.imageDataUrl ?? undefined}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent px-3 pb-2 pt-6 text-2xs text-white">
|
||||
Automatic preview from {preview.imageDomain}
|
||||
<div className={cn("relative w-80 max-w-full shrink-0", className)}>
|
||||
<Attachment
|
||||
className="w-full no-underline shadow-none"
|
||||
data-link-preview={preview.kind}
|
||||
orientation={showImage ? "vertical" : "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={`Automatic preview from ${preview.imageDomain}`}
|
||||
className="aspect-[1.91/1] w-full object-cover"
|
||||
src={preview.imageDataUrl ?? undefined}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent px-3 pb-2 pt-6 text-2xs text-white">
|
||||
Automatic preview from {preview.imageDomain}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!showImage ? (
|
||||
<AttachmentMedia className="link-preview-media">
|
||||
<LinkPreviewLogo preview={preview} />
|
||||
</AttachmentMedia>
|
||||
) : null}
|
||||
<AttachmentContent className={showImage ? "w-full" : undefined}>
|
||||
<div className="truncate text-xs font-medium leading-4 text-muted-foreground">
|
||||
{preview.provider}
|
||||
<span aria-hidden="true"> · </span>
|
||||
{preview.typeLabel}
|
||||
</div>
|
||||
<AttachmentTitle>{preview.title}</AttachmentTitle>
|
||||
</AttachmentContent>
|
||||
<AttachmentActions>
|
||||
<ExternalLink
|
||||
aria-hidden="true"
|
||||
className="h-4 w-4 text-muted-foreground opacity-0 transition-opacity group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100"
|
||||
/>
|
||||
</AttachmentActions>
|
||||
<AttachmentTrigger asChild>
|
||||
<a
|
||||
aria-label={`Open ${preview.provider} ${preview.typeLabel}: ${preview.title}`}
|
||||
href={preview.href}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
) : null}
|
||||
{!showImage ? (
|
||||
<AttachmentMedia className="link-preview-media">
|
||||
<LinkPreviewLogo preview={preview} />
|
||||
</AttachmentMedia>
|
||||
) : null}
|
||||
<AttachmentContent className={showImage ? "w-full" : undefined}>
|
||||
<div className="truncate text-xs font-medium leading-4 text-muted-foreground">
|
||||
{preview.provider}
|
||||
<span aria-hidden="true"> · </span>
|
||||
{preview.typeLabel}
|
||||
</div>
|
||||
<AttachmentTitle>{preview.title}</AttachmentTitle>
|
||||
</AttachmentContent>
|
||||
<AttachmentTrigger asChild>
|
||||
<a
|
||||
aria-label={`Open ${preview.provider} ${preview.typeLabel}: ${preview.title}`}
|
||||
href={preview.href}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open {preview.provider} {preview.typeLabel}: {preview.title}
|
||||
</span>
|
||||
</a>
|
||||
</AttachmentTrigger>
|
||||
</Attachment>
|
||||
{onRemove ? (
|
||||
<button
|
||||
aria-label="Remove previews for everyone"
|
||||
className="absolute left-full top-0 z-20 ml-1 flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-destructive focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring group-hover/message:opacity-100"
|
||||
onClick={onRemove}
|
||||
title="Remove previews for everyone"
|
||||
type="button"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open {preview.provider} {preview.typeLabel}: {preview.title}
|
||||
</span>
|
||||
</a>
|
||||
</AttachmentTrigger>
|
||||
</Attachment>
|
||||
<X aria-hidden="true" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1946,6 +1946,8 @@ function MarkdownInner({
|
||||
}
|
||||
|
||||
const resolvedLinkPreviews = useResolvedLinkPreviews(linkPreviews);
|
||||
const previewNoun =
|
||||
resolvedLinkPreviews.length === 1 ? "preview" : "previews";
|
||||
|
||||
// When a config-nudge suppresses the prose (selectProseOrNudge returns
|
||||
// null), skip the parse entirely — it would be thrown away unrendered.
|
||||
@@ -1996,27 +1998,22 @@ function MarkdownInner({
|
||||
</AttachmentGroup>
|
||||
) : null}
|
||||
{resolvedLinkPreviews.length > 0 ? (
|
||||
<div className="space-y-1.5" data-link-preview-container="">
|
||||
<AttachmentGroup
|
||||
className="max-w-full flex-wrap overflow-visible pb-0"
|
||||
data-link-preview-list=""
|
||||
>
|
||||
{resolvedLinkPreviews.map((preview) => (
|
||||
<LinkPreviewAttachment key={preview.href} preview={preview} />
|
||||
))}
|
||||
</AttachmentGroup>
|
||||
{onRemoveLinkPreviewsForEveryone ? (
|
||||
<div className="text-2xs text-muted-foreground">
|
||||
<button
|
||||
className="hover:text-destructive hover:underline"
|
||||
onClick={() => setRemovePreviewDialogOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
Remove previews for everyone
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<AttachmentGroup
|
||||
className="max-w-full flex-wrap overflow-visible pb-0"
|
||||
data-link-preview-list=""
|
||||
>
|
||||
{resolvedLinkPreviews.map((preview) => (
|
||||
<LinkPreviewAttachment
|
||||
key={preview.href}
|
||||
onRemove={
|
||||
onRemoveLinkPreviewsForEveryone
|
||||
? () => setRemovePreviewDialogOpen(true)
|
||||
: undefined
|
||||
}
|
||||
preview={preview}
|
||||
/>
|
||||
))}
|
||||
</AttachmentGroup>
|
||||
) : null}
|
||||
{onRemoveLinkPreviewsForEveryone ? (
|
||||
<AlertDialog
|
||||
@@ -2026,12 +2023,18 @@ function MarkdownInner({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Remove previews for everyone?
|
||||
Remove {previewNoun} for everyone?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This permanently removes all generated link previews from
|
||||
this message for supporting clients. The links stay in the
|
||||
message. This can't be undone.
|
||||
No one will see{" "}
|
||||
{resolvedLinkPreviews.length === 1
|
||||
? "the preview"
|
||||
: "the previews"}{" "}
|
||||
on this message anymore.{" "}
|
||||
{resolvedLinkPreviews.length === 1
|
||||
? "The link itself will stay"
|
||||
: "The links themselves will stay"}{" "}
|
||||
in the message. This can't be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -2051,7 +2054,7 @@ function MarkdownInner({
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
Remove previews
|
||||
Remove {previewNoun}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user