diff --git a/desktop/src/shared/ui/compact-link-preview-attachment.tsx b/desktop/src/shared/ui/compact-link-preview-attachment.tsx
index e14e440a8..018ec7201 100644
--- a/desktop/src/shared/ui/compact-link-preview-attachment.tsx
+++ b/desktop/src/shared/ui/compact-link-preview-attachment.tsx
@@ -1,8 +1,7 @@
-import { Globe, X } from "lucide-react";
+import { Globe } from "lucide-react";
import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews";
import { cn } from "@/shared/lib/cn";
-import { Button } from "@/shared/ui/button";
import {
Attachment,
AttachmentContent,
@@ -11,6 +10,7 @@ import {
AttachmentTitle,
AttachmentTrigger,
} from "@/shared/ui/attachment";
+import { LinkPreviewControls } from "@/shared/ui/link-preview-controls";
function LinearLogo({ className }: { className?: string }) {
return (
@@ -136,10 +136,12 @@ export function CompactLinkPreviewAttachment({
className,
onRemove,
preview,
+ showControls = false,
}: {
className?: string;
onRemove?: () => void;
preview: ResolvedLinkPreview;
+ showControls?: boolean;
}) {
const reserveImage = preview.imageState !== "none";
const showImage = preview.imageState === "image";
@@ -227,18 +229,8 @@ export function CompactLinkPreviewAttachment({
- {onRemove ? (
-
+ {showControls ? (
+
) : null}
);
diff --git a/desktop/src/shared/ui/link-preview-attachment.tsx b/desktop/src/shared/ui/link-preview-attachment.tsx
index c125f5898..88b61fdaa 100644
--- a/desktop/src/shared/ui/link-preview-attachment.tsx
+++ b/desktop/src/shared/ui/link-preview-attachment.tsx
@@ -11,11 +11,13 @@ export function LinkPreviewAttachment({
ImageLightbox,
onRemove,
preview,
+ showControls,
}: {
className?: string;
ImageLightbox: LinkPreviewImageLightboxComponent;
onRemove?: () => void;
preview: ResolvedLinkPreview;
+ showControls?: boolean;
}) {
const style = useLinkPreviewStyle();
if (style === "rich") {
@@ -25,6 +27,7 @@ export function LinkPreviewAttachment({
ImageLightbox={ImageLightbox}
onRemove={onRemove}
preview={preview}
+ showControls={showControls}
/>
);
}
@@ -34,6 +37,7 @@ export function LinkPreviewAttachment({
className={className}
onRemove={onRemove}
preview={preview}
+ showControls={showControls}
/>
);
}
diff --git a/desktop/src/shared/ui/link-preview-controls.tsx b/desktop/src/shared/ui/link-preview-controls.tsx
new file mode 100644
index 000000000..40ab1d3e1
--- /dev/null
+++ b/desktop/src/shared/ui/link-preview-controls.tsx
@@ -0,0 +1,125 @@
+import { EllipsisVertical, EyeOff } from "lucide-react";
+import { toast } from "sonner";
+
+import { useAppShell } from "@/app/AppShellContext";
+import {
+ setLinkPreviewStyle,
+ type LinkPreviewStyle,
+ useLinkPreviewStyle,
+} from "@/shared/lib/linkPreviewStylePreference";
+import { cn } from "@/shared/lib/cn";
+import { Button } from "@/shared/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/shared/ui/dropdown-menu";
+
+const CONTROL_BUTTON_CLASS =
+ "h-5 w-5 rounded-full text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/message:opacity-100 data-[state=open]:opacity-100";
+
+const LINK_PREVIEW_STYLE_OPTIONS: {
+ value: LinkPreviewStyle;
+ label: string;
+}[] = [
+ { value: "rich", label: "Rich" },
+ { value: "compact", label: "Compact" },
+];
+
+export function LinkPreviewControls({
+ onRemove,
+ placement = "right",
+}: {
+ onRemove?: () => void;
+ placement?: "left" | "right";
+}) {
+ const style = useLinkPreviewStyle();
+ const { onOpenSettings } = useAppShell();
+
+ const handleStyleChange = (nextStyle: string) => {
+ if (
+ (nextStyle !== "rich" && nextStyle !== "compact") ||
+ nextStyle === style
+ ) {
+ return;
+ }
+
+ setLinkPreviewStyle(nextStyle);
+ toast.success(
+ `Link previews set to ${nextStyle === "rich" ? "Rich" : "Compact"}.`,
+ {
+ action: onOpenSettings
+ ? {
+ label: "Appearance",
+ onClick: () => onOpenSettings("appearance"),
+ }
+ : undefined,
+ description:
+ "You can always modify this and other settings in Appearance.",
+ },
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+ Link display
+
+
+ {LINK_PREVIEW_STYLE_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+ {onRemove ? (
+ <>
+
+
+
+ Remove preview
+
+ >
+ ) : null}
+
+
+
+ );
+}
diff --git a/desktop/src/shared/ui/link-preview-list.tsx b/desktop/src/shared/ui/link-preview-list.tsx
index 2c36e7596..bcfa3c9ae 100644
--- a/desktop/src/shared/ui/link-preview-list.tsx
+++ b/desktop/src/shared/ui/link-preview-list.tsx
@@ -32,7 +32,7 @@ export function LinkPreviewList({
if (removed || previews.length === 0) return null;
const previewNoun = previews.length === 1 ? "preview" : "previews";
- const removeButtonIndex = style === "compact" ? previews.length - 1 : 0;
+ const controlsIndex = 0;
return (
<>
setDialogOpen(true)
: undefined
}
preview={preview}
+ showControls={index === controlsIndex}
/>
))}
diff --git a/desktop/src/shared/ui/rich-link-preview-attachment.tsx b/desktop/src/shared/ui/rich-link-preview-attachment.tsx
index c906e1d4e..205e0e76e 100644
--- a/desktop/src/shared/ui/rich-link-preview-attachment.tsx
+++ b/desktop/src/shared/ui/rich-link-preview-attachment.tsx
@@ -1,10 +1,10 @@
-import { ChevronDown, ChevronUp, X } from "lucide-react";
+import { ChevronDown, ChevronUp } from "lucide-react";
import type { ComponentType, ReactNode } from "react";
import { useState } from "react";
import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews";
import { cn } from "@/shared/lib/cn";
-import { Button } from "@/shared/ui/button";
+import { LinkPreviewControls } from "@/shared/ui/link-preview-controls";
export type LinkPreviewImageLightboxProps = {
alt: string;
@@ -129,11 +129,13 @@ function TweetPreview({
ImageLightbox,
onRemove,
preview,
+ showControls,
}: {
className?: string;
ImageLightbox: LinkPreviewImageLightboxComponent;
onRemove?: () => void;
preview: ResolvedLinkPreview;
+ showControls: boolean;
}) {
const [contentExpanded, setContentExpanded] = useState(true);
const reserveImage = preview.imageState !== "none";
@@ -173,7 +175,7 @@ function TweetPreview({
{contentExpanded && reserveImage ? (
@@ -193,18 +195,8 @@ function TweetPreview({
{contentExpanded ? "Show less" : "Show more"}
) : null}
- {onRemove ? (
-
+ {showControls ? (
+
) : null}
);
@@ -215,11 +207,13 @@ export function RichLinkPreviewAttachment({
ImageLightbox,
onRemove,
preview,
+ showControls = false,
}: {
className?: string;
ImageLightbox: LinkPreviewImageLightboxComponent;
onRemove?: () => void;
preview: ResolvedLinkPreview;
+ showControls?: boolean;
}) {
const [contentExpanded, setContentExpanded] = useState(true);
@@ -230,6 +224,7 @@ export function RichLinkPreviewAttachment({
ImageLightbox={ImageLightbox}
onRemove={onRemove}
preview={preview}
+ showControls={showControls}
/>
);
}
@@ -277,7 +272,7 @@ export function RichLinkPreviewAttachment({
{contentExpanded && reserveImage ? (
@@ -297,18 +292,8 @@ export function RichLinkPreviewAttachment({
{contentExpanded ? "Show less" : "Show more"}
) : null}
- {onRemove ? (
-
+ {showControls ? (
+
) : null}
);