Fix desktop glass chrome and inbox previews (#793)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
thomaspblock
2026-05-30 20:27:16 -07:00
committed by GitHub
co-authored by Cursor
parent 9b9cf46127
commit bc23620fcf
10 changed files with 146 additions and 93 deletions
@@ -93,7 +93,7 @@ export function AgentSessionThreadPanel({
{!isOverlay ? (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/45 backdrop-blur-xl after:absolute after:bottom-0 after:left-0 after:top-10 after:w-px after:bg-border/80 supports-[backdrop-filter]:bg-background/35"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/75 backdrop-blur-md after:absolute after:bottom-0 after:left-0 after:top-10 after:w-px after:bg-border/80 supports-[backdrop-filter]:bg-background/65 dark:bg-background/45 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/35"
/>
) : null}
@@ -101,7 +101,7 @@ export function AgentSessionThreadPanel({
className={cn(
"z-50 flex cursor-default select-none items-center gap-3 px-4",
isOverlay
? "relative min-h-[44px] shrink-0 border-b border-border/70 bg-background/70 py-2.5 backdrop-blur-xl supports-[backdrop-filter]:bg-background/55"
? "relative min-h-[44px] shrink-0 border-b border-border/70 bg-background/80 py-2.5 backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55"
: "absolute inset-x-0 top-11 min-h-[32px] py-[4px]",
)}
data-tauri-drag-region
+1 -1
View File
@@ -150,7 +150,7 @@ export function ChatHeader({
}
return (
<div className="relative z-30 h-[76px] -mb-[76px] bg-background/70 pt-[42px] backdrop-blur-xl supports-[backdrop-filter]:bg-background/55">
<div className="relative z-30 h-[76px] -mb-[76px] bg-background/80 pt-[42px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55">
{header}
</div>
);
@@ -214,7 +214,7 @@ export function InboxDetailPane({
<div className="relative min-h-0 flex-1 overflow-hidden">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/45 backdrop-blur-xl supports-[backdrop-filter]:bg-background/35"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/75 backdrop-blur-md supports-[backdrop-filter]:bg-background/65 dark:bg-background/45 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/35"
/>
<div className="absolute inset-x-0 top-[38px] z-50 flex min-h-[32px] items-center justify-between gap-3 py-[4px] pl-6 pr-3">
<div className="min-w-0">
+12 -5
View File
@@ -7,6 +7,7 @@ import {
} from "@/features/home/lib/inbox";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Markdown } from "@/shared/ui/markdown";
import {
DropdownMenu,
DropdownMenuContent,
@@ -47,7 +48,7 @@ export function InboxListPane({
<section className="relative flex min-h-0 min-w-0 flex-col overflow-hidden bg-background/60">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/45 backdrop-blur-xl supports-[backdrop-filter]:bg-background/35"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/75 backdrop-blur-md supports-[backdrop-filter]:bg-background/65 dark:bg-background/45 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/35"
/>
<div className="absolute inset-x-0 top-[42px] z-50 min-h-[32px] px-5 py-[4px]">
<div className="flex min-w-0 items-center justify-between gap-3">
@@ -159,16 +160,22 @@ export function InboxListPane({
</span>
</div>
<p
<div
className={cn(
"mt-0.5 line-clamp-2 text-sm leading-5",
"mt-0.5 line-clamp-2 text-sm leading-5 [&_*]:inline [&_a]:font-medium [&_a]:text-current [&_br]:hidden [&_p]:inline",
isDone
? "font-normal text-muted-foreground"
: "font-semibold text-foreground",
)}
>
{item.preview}
</p>
<Markdown
className="inline max-w-full text-inherit"
content={item.preview}
interactive={false}
mentionNames={item.mentionNames}
tight
/>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span
@@ -654,7 +654,7 @@ export function MessageComposer({
/>
<div className="relative flex w-full flex-col gap-3">
<form
className="relative isolate rounded-2xl border border-border/50 bg-background/70 px-3 pb-2 pt-3 shadow-[0_4px_24px_rgba(0,0,0,0.08)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/55 dark:shadow-[0_4px_24px_rgba(0,0,0,0.35)] sm:px-4"
className="relative isolate rounded-2xl border border-border/50 bg-background/80 px-3 pb-2 pt-3 shadow-[0_4px_24px_rgba(0,0,0,0.08)] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:shadow-[0_4px_24px_rgba(0,0,0,0.35)] dark:supports-[backdrop-filter]:bg-background/55 sm:px-4"
data-testid="message-composer"
onDragEnter={media.handleDragEnter}
onDragLeave={media.handleDragLeave}
@@ -198,7 +198,7 @@ export function MessageThreadPanel({
className={cn(
"z-50 flex cursor-default select-none items-center gap-3 px-3",
isOverlay
? "relative min-h-[44px] shrink-0 bg-background/70 py-[6px] backdrop-blur-xl supports-[backdrop-filter]:bg-background/55"
? "relative min-h-[44px] shrink-0 bg-background/80 py-[6px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55"
: "absolute inset-x-0 top-11 min-h-[32px] py-[4px]",
)}
data-tauri-drag-region
@@ -149,7 +149,10 @@ export const MessageTimeline = React.memo(function MessageTimeline({
onScroll={syncScrollState}
ref={scrollContainerRef}
>
<div className="flex w-full flex-col gap-2 pt-12" ref={contentRef}>
<div
className="flex w-full flex-col gap-2 pt-[76px]"
ref={contentRef}
>
<div ref={topSentinelRef} aria-hidden className="h-px" />
{isFetchingOlder ? (
+78 -77
View File
@@ -600,7 +600,7 @@ export function AppSidebar({
</SidebarMenu>
</SidebarHeader>
<div className="relative flex min-h-0 flex-1 flex-col">
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
{unreadAboveCount > 0 ? (
<MoreUnreadButton
count={unreadAboveCount}
@@ -610,7 +610,7 @@ export function AppSidebar({
testId="sidebar-more-unread-above"
/>
) : null}
<SidebarContent ref={scrollRef}>
<SidebarContent className="pb-32" ref={scrollRef}>
{isLoading ? (
<SidebarGroup>
<SidebarGroupLabel>Channels</SidebarGroupLabel>
@@ -713,6 +713,7 @@ export function AppSidebar({
{unreadBelowCount > 0 ? (
<MoreUnreadButton
bottomClassName="bottom-28"
count={unreadBelowCount}
icon={<ArrowDown />}
onClick={scrollToNextBelow}
@@ -720,88 +721,88 @@ export function AppSidebar({
testId="sidebar-more-unread-below"
/>
) : null}
</div>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<div
className="rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-accent/70 focus-within:bg-sidebar-accent/70"
data-testid="sidebar-profile-card"
>
<div className="flex min-w-0 items-center gap-3">
<div className="relative shrink-0">
<ProfileAvatar
avatarUrl={profile?.avatarUrl ?? null}
className="h-10 w-10 rounded-2xl text-sm"
iconClassName="h-5 w-5"
label={resolvedDisplayName}
testId="sidebar-profile-avatar"
/>
<span
aria-label={getPresenceLabel(selfPresenceStatus)}
className="absolute -bottom-0.5 -right-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-sidebar"
data-testid="self-presence-badge"
role="img"
>
<PresenceDot
className="h-2.5 w-2.5"
status={selfPresenceStatus}
<SidebarFooter className="absolute inset-x-0 bottom-0 z-30 bg-sidebar/55 shadow-[0_-16px_40px_hsl(var(--sidebar)/0.55)] backdrop-blur-xl supports-[backdrop-filter]:bg-sidebar/45 dark:bg-sidebar/45 dark:shadow-[0_-16px_44px_rgba(0,0,0,0.42)] dark:supports-[backdrop-filter]:bg-sidebar/35">
<SidebarMenu>
<SidebarMenuItem>
<div
className="rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-accent/35 focus-within:bg-sidebar-accent/35 dark:hover:bg-sidebar-accent/25 dark:focus-within:bg-sidebar-accent/25"
data-testid="sidebar-profile-card"
>
<div className="flex min-w-0 items-center gap-3">
<div className="relative shrink-0">
<ProfileAvatar
avatarUrl={profile?.avatarUrl ?? null}
className="h-10 w-10 rounded-2xl text-sm"
iconClassName="h-5 w-5"
label={resolvedDisplayName}
testId="sidebar-profile-avatar"
/>
</span>
</div>
<div className="min-w-0 flex-1">
<ProfilePopover
open={profilePopoverOpen}
onOpenChange={setProfilePopoverOpen}
displayName={resolvedDisplayName}
nip05={profile?.nip05Handle}
avatarUrl={profile?.avatarUrl ?? null}
currentStatus={selfPresenceStatus}
isStatusPending={isPresencePending}
userStatusText={selfUserStatus?.text}
userStatusEmoji={selfUserStatus?.emoji}
onSetStatus={onSetPresenceStatus ?? (() => {})}
onSetUserStatus={onSetUserStatus}
onClearUserStatus={onClearUserStatus}
onOpenSettings={onSelectSettings}
>
<button
className="block w-full min-w-0 text-left text-sidebar-foreground"
data-testid="open-settings"
type="button"
<span
aria-label={getPresenceLabel(selfPresenceStatus)}
className="absolute -bottom-0.5 -right-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-sidebar"
data-testid="self-presence-badge"
role="img"
>
<p
className="truncate text-sm font-semibold text-current"
data-testid="sidebar-profile-name"
<PresenceDot
className="h-2.5 w-2.5"
status={selfPresenceStatus}
/>
</span>
</div>
<div className="min-w-0 flex-1">
<ProfilePopover
open={profilePopoverOpen}
onOpenChange={setProfilePopoverOpen}
displayName={resolvedDisplayName}
nip05={profile?.nip05Handle}
avatarUrl={profile?.avatarUrl ?? null}
currentStatus={selfPresenceStatus}
isStatusPending={isPresencePending}
userStatusText={selfUserStatus?.text}
userStatusEmoji={selfUserStatus?.emoji}
onSetStatus={onSetPresenceStatus ?? (() => {})}
onSetUserStatus={onSetUserStatus}
onClearUserStatus={onClearUserStatus}
onOpenSettings={onSelectSettings}
>
<button
className="block w-full min-w-0 text-left text-sidebar-foreground"
data-testid="open-settings"
type="button"
>
{resolvedDisplayName}
<p
className="truncate text-sm font-semibold text-current"
data-testid="sidebar-profile-name"
>
{resolvedDisplayName}
</p>
</button>
</ProfilePopover>
<WorkspaceSwitcher
activeWorkspace={activeWorkspace}
onAddWorkspace={onOpenAddWorkspace}
onRemoveWorkspace={onRemoveWorkspace}
onSwitchWorkspace={onSwitchWorkspace}
onUpdateWorkspace={onUpdateWorkspace}
variant="profile"
workspaces={workspaces}
/>
{selfUserStatus?.text || selfUserStatus?.emoji ? (
<p className="mt-0.5 truncate text-xs text-sidebar-foreground/50">
{selfUserStatus.emoji ? (
<span className="mr-1">{selfUserStatus.emoji}</span>
) : null}
{selfUserStatus.text}
</p>
</button>
</ProfilePopover>
<WorkspaceSwitcher
activeWorkspace={activeWorkspace}
onAddWorkspace={onOpenAddWorkspace}
onRemoveWorkspace={onRemoveWorkspace}
onSwitchWorkspace={onSwitchWorkspace}
onUpdateWorkspace={onUpdateWorkspace}
variant="profile"
workspaces={workspaces}
/>
{selfUserStatus?.text || selfUserStatus?.emoji ? (
<p className="mt-0.5 truncate text-xs text-sidebar-foreground/50">
{selfUserStatus.emoji ? (
<span className="mr-1">{selfUserStatus.emoji}</span>
) : null}
{selfUserStatus.text}
</p>
) : null}
) : null}
</div>
</div>
</div>
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</div>
<CreateChannelDialog
channelKind={createDialogKind}
@@ -6,12 +6,14 @@ const MORE_UNREAD_BUTTON_CLASS =
"h-7 min-h-7 gap-1.5 rounded-full border-0 bg-primary px-2.5 text-[11px] font-medium text-primary-foreground shadow-md hover:bg-primary/90 [&_svg]:size-3.5";
export function MoreUnreadButton({
bottomClassName = "bottom-0",
count,
icon,
onClick,
position,
testId,
}: {
bottomClassName?: string;
count: number;
icon: React.ReactNode;
onClick: () => void;
@@ -20,7 +22,7 @@ export function MoreUnreadButton({
}) {
return (
<div
className={`pointer-events-none absolute inset-x-0 z-10 flex justify-center py-1 ${position === "top" ? "top-0" : "bottom-0"}`}
className={`pointer-events-none absolute inset-x-0 z-10 flex justify-center py-1 ${position === "top" ? "top-0" : bottomClassName}`}
>
<Button
className={`pointer-events-auto ${MORE_UNREAD_BUTTON_CLASS}`}
+43 -3
View File
@@ -59,6 +59,7 @@ type MarkdownProps = {
compact?: boolean;
content: string;
imetaByUrl?: ImetaLookup;
interactive?: boolean;
mentionNames?: string[];
mentionPubkeysByName?: Record<string, string>;
searchQuery?: string;
@@ -204,6 +205,7 @@ function createMarkdownComponents(
onOpenMessageLink: (link: ParsedMessageLink) => void,
imetaByUrl?: ImetaLookup,
mentionPubkeysByName?: Record<string, string>,
interactive = true,
): Components {
const paragraphClassName =
variant === "tight"
@@ -220,6 +222,10 @@ function createMarkdownComponents(
return {
a: ({ children, href, ...props }) => {
if (!interactive) {
return <span className="font-medium text-current">{children}</span>;
}
// Intercept `sprout://message?channel=…&id=…` links so a click navigates
// in-app instead of opening the URL in the OS browser. http(s) links
// continue to use the existing target="_blank" behavior.
@@ -312,6 +318,13 @@ function createMarkdownComponents(
hr: () => <hr className="border-border/80" />,
img: ({ alt, src }) => {
const resolvedSrc = src ? rewriteRelayUrl(src) : src;
if (!interactive) {
const fallbackLabel = resolvedSrc?.endsWith(".mp4")
? "Video attachment"
: "Image attachment";
return <span>{alt?.trim() || fallbackLabel}</span>;
}
if (resolvedSrc?.endsWith(".mp4")) {
// Look up poster frame from imeta tags (NIP-71 `image` field).
// Fall back to `thumb` for compatibility with older events.
@@ -423,7 +436,12 @@ function createMarkdownComponents(
return <p className={paragraphClassName}>{children}</p>;
},
pre: ({ children }) => <MarkdownCodeBlock>{children}</MarkdownCodeBlock>,
pre: ({ children }) =>
interactive ? (
<MarkdownCodeBlock>{children}</MarkdownCodeBlock>
) : (
<span>{children}</span>
),
strong: ({ children }) => (
<strong className="font-semibold">{children}</strong>
),
@@ -463,6 +481,10 @@ function createMarkdownComponents(
</span>
);
if (!interactive) {
return mentionNode;
}
return pubkey ? (
<UserProfilePopover pubkey={pubkey} triggerElement="span">
{mentionNode}
@@ -480,7 +502,7 @@ function createMarkdownComponents(
c.name.toLowerCase() === channelName.toLowerCase(),
);
if (channel) {
if (channel && interactive) {
return (
<button
type="button"
@@ -519,6 +541,14 @@ function createMarkdownComponents(
const channelLabel = channel?.name ?? "channel";
const shortId = messageId.slice(0, 6);
if (!interactive) {
return (
<span data-message-link="">
#{channelLabel} · {shortId}
</span>
);
}
return (
<button
type="button"
@@ -543,6 +573,7 @@ function MarkdownInner({
compact = false,
content,
imetaByUrl,
interactive = true,
mentionNames,
mentionPubkeysByName,
searchQuery,
@@ -576,8 +607,16 @@ function MarkdownInner({
},
imetaByUrl,
mentionPubkeysByName,
interactive,
),
[goChannel, variant, channels, imetaByUrl, mentionPubkeysByName],
[
goChannel,
variant,
channels,
imetaByUrl,
mentionPubkeysByName,
interactive,
],
);
// biome-ignore lint/suspicious/noExplicitAny: PluggableList type not directly importable
@@ -686,6 +725,7 @@ export const Markdown = React.memo(
prev.content === next.content &&
prev.className === next.className &&
prev.compact === next.compact &&
prev.interactive === next.interactive &&
prev.tight === next.tight &&
prev.mentionPubkeysByName === next.mentionPubkeysByName &&
shallowArrayEqual(prev.mentionNames, next.mentionNames) &&