Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions desktop/src/shared/ui/compact-link-preview-attachment.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ImageOff } from "lucide-react";
import { useState } from "react";
import { useRef, useState } from "react";

import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews";
import { cn } from "@/shared/lib/cn";
Expand All @@ -13,6 +13,7 @@ import {
} from "@/shared/ui/attachment";
import { LinkPreviewControls } from "@/shared/ui/link-preview-controls";
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
import { useSmoothCorners } from "@/shared/ui/smoothCorners";

function getHostname(preview: ResolvedLinkPreview): string {
if (preview.href.startsWith("buzz://")) return preview.provider;
Expand Down Expand Up @@ -64,6 +65,13 @@ export function CompactLinkPreviewAttachment({
const imageSrc =
preview.imageState === "image" ? (preview.imageDataUrl ?? null) : null;
const [failedImageSrc, setFailedImageSrc] = useState<string | null>(null);
// The shell clips its corners with a smooth-corner (squircle) path, and the
// thumbnail sits flush against its left edge — so the thumbnail's left
// corners are carved by that path while its right corners are its own.
// A plain border-radius can never match a squircle of the same radius, so
// give the thumbnail the same treatment: both sides then share one curve.
const thumbnailRef = useRef<HTMLDivElement | null>(null);
useSmoothCorners(thumbnailRef);
const showImage = Boolean(imageSrc && failedImageSrc !== imageSrc);
const showFallback =
preview.imageState === "fallback" || Boolean(imageSrc && !showImage);
Expand All @@ -88,8 +96,9 @@ export function CompactLinkPreviewAttachment({
>
{reserveImage ? (
<AttachmentMedia
ref={thumbnailRef}
aria-hidden={showImage ? undefined : "true"}
className="aspect-auto h-16 w-26 rounded-xl bg-muted"
className="aspect-auto h-16 w-26 rounded-2xl bg-muted"
data-link-preview-thumbnail=""
variant="image"
>
Expand Down
33 changes: 33 additions & 0 deletions desktop/src/shared/ui/smoothCorners.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
/**
* Corner smoothing ("squircle") for media surfaces.
*
* CSS `border-radius` can only draw a circular arc. This helper reads an
* element's computed radius and replaces the shape with an equivalent
* *smoothed* corner, applied at runtime as an inline `clip-path`. The radius
* token stays the source of truth — `rounded-2xl` still means a 16px corner,
* it just renders as a smoothed 16px corner (the curve starts 1.6x further
* along the edge, so it reads wider and softer than a plain arc).
*
* INVARIANT — flush children must share the parent's corner treatment.
*
* A smooth-cornered parent clips its subtree. Any child that sits *flush*
* against a clipped edge therefore has those corners carved by the parent's
* smoothed path, while its remaining corners are drawn by its own
* `border-radius`. A plain arc and a smoothed corner of the *same* radius are
* different shapes, so such a child renders visibly mismatched corners — the
* number matching is not enough.
*
* So when a child is flush to a smooth-cornered parent (typically because the
* parent zeroes its padding, e.g. `p-0`), either:
* - give the child `useSmoothCorners` too, so both sides share one curve
* (see `compact-link-preview-attachment.tsx`), or
* - give the child no radius on the flush side and let the parent's clip own
* that silhouette entirely.
*
* Inset children are unaffected: they never share a corner with the clip.
*
* This cannot be caught by a lint rule — "flush" is a runtime layout fact, not
* something visible in the source. Guard it with `expectSmoothCorners()` from
* `desktop/tests/helpers/css.ts` instead.
*/

import * as React from "react";

export const SMOOTH_CORNER_SMOOTHING = 0.6;
Expand Down
12 changes: 12 additions & 0 deletions desktop/tests/e2e/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,18 @@ test("sent link preview media uses the authenticated proxy in compact and rich c
.poll(() => compactThumbnail.evaluate((image) => image.naturalWidth))
.toBe(40);

// The compact thumbnail sits flush against the card shell's left edge, so the
// shell's smooth-corner clip carves those corners. It must therefore carry
// the same treatment itself, or its right corners render as a plain arc
// against the shell's smoothed left corners. See the invariant in
// `shared/ui/smoothCorners.ts`.
const compactThumbnailFrame = compactPreview
.locator("[data-link-preview-thumbnail]")
.first();
await expectCornerRadiusPx(compactPreview, 16);
await expectCornerRadiusPx(compactThumbnailFrame, 16);
await expectSmoothCorners(compactThumbnailFrame);

await openSettings(page, "appearance");
await page.getByTestId("link-preview-style-trigger").click();
await page.getByTestId("link-preview-style-rich").click();
Expand Down
Loading