mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix desktop glass chrome and inbox previews (#793)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
Reference in New Issue
Block a user