mirror of
https://github.com/block/buzz.git
synced 2026-11-04 08:21:48 +00:00
fix(desktop): refine pulse composer and search UI
Polish the Pulse composer, search state, and note typography so the view aligns more closely with channel message presentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,6 +35,7 @@ type ForumComposerProps = {
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
disabled?: boolean;
|
||||
header?: React.ReactNode;
|
||||
isSending?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSubmit: (
|
||||
@@ -53,6 +54,7 @@ export function ForumComposer({
|
||||
className,
|
||||
placeholder,
|
||||
disabled,
|
||||
header,
|
||||
isSending,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
@@ -355,6 +357,7 @@ export function ForumComposer({
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{media.isDragOver && <DropZoneOverlay />}
|
||||
{header ? <div className="mb-2">{header}</div> : null}
|
||||
<ChannelAutocomplete
|
||||
onSelect={applyChannelInsert}
|
||||
position={autocompletePosition}
|
||||
|
||||
@@ -44,17 +44,21 @@ export function NoteCard({
|
||||
const avatarUrl = profile?.avatarUrl ?? null;
|
||||
|
||||
return (
|
||||
<article className="group flex gap-3 rounded-2xl px-1 py-4 transition-colors hover:bg-muted/20 sm:px-2">
|
||||
<div className="relative shrink-0 pt-1">
|
||||
<UserAvatar avatarUrl={avatarUrl} displayName={displayName} />
|
||||
<article className="group flex items-start gap-2.5 rounded-2xl px-1 py-4 transition-colors hover:bg-muted/20 sm:px-2">
|
||||
<div className="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatarUrl={avatarUrl}
|
||||
className="!h-9 !w-9 shrink-0"
|
||||
displayName={displayName}
|
||||
/>
|
||||
{isAgent ? (
|
||||
<Bot className="absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-background p-0.5 text-muted-foreground" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-semibold leading-none">
|
||||
<div className="min-w-0 flex-1 space-y-0">
|
||||
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0">
|
||||
<span className="truncate text-sm font-semibold leading-none tracking-tight">
|
||||
{displayName}
|
||||
</span>
|
||||
{isAgent ? (
|
||||
@@ -67,7 +71,7 @@ export function NoteCard({
|
||||
{profile.nip05Handle}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">
|
||||
<span className="shrink-0 text-xs text-muted-foreground/70">
|
||||
{formatRelativeTime(note.createdAt)}
|
||||
</span>
|
||||
{!isOwnNote ? (
|
||||
@@ -97,8 +101,8 @@ export function NoteCard({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-1.5 text-sm leading-relaxed text-foreground">
|
||||
<Markdown content={note.content} />
|
||||
<div className="mt-0.5 text-sm text-foreground">
|
||||
<Markdown content={note.content} tight />
|
||||
</div>
|
||||
|
||||
{!isOwnNote ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, Filter, RefreshCw } from "lucide-react";
|
||||
import { Check, Filter, Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { useRelayAgentsQuery } from "@/features/agents/hooks";
|
||||
@@ -28,14 +28,15 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
|
||||
type PulseTab = "foryou" | "people" | "agents" | "mine";
|
||||
type PulseTab = "search" | "foryou" | "people" | "agents" | "mine";
|
||||
|
||||
const tabTriggerClassName =
|
||||
"h-6 rounded-full px-3 py-0 text-[11px] font-semibold shadow-none transition-colors text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm";
|
||||
"h-6 rounded-full px-3 py-0 text-[11px] font-semibold shadow-none transition-colors !text-muted-foreground hover:!text-foreground data-[state=active]:bg-background data-[state=active]:!text-foreground data-[state=active]:shadow-sm dark:!text-white/35 dark:hover:!text-white/70 dark:data-[state=active]:!text-white";
|
||||
|
||||
type PulseViewProps = {
|
||||
currentPubkey?: string;
|
||||
@@ -136,6 +137,7 @@ function AgentFilter({
|
||||
export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
const [activeTab, setActiveTab] = React.useState<PulseTab>("foryou");
|
||||
const [agentFilter, setAgentFilter] = React.useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
|
||||
// ── Contact list & follow state ────────────────────────────────────────
|
||||
const contactListQuery = useContactListQuery(currentPubkey);
|
||||
@@ -274,11 +276,6 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
? agentTimelineQuery
|
||||
: myNotesQuery;
|
||||
const isLoading = activeQuery.isLoading;
|
||||
const isRefetching = activeQuery.isRefetching;
|
||||
|
||||
function handleRefresh() {
|
||||
void activeQuery.refetch();
|
||||
}
|
||||
|
||||
function handleFollow(pubkey: string) {
|
||||
followMutation.mutate(pubkey);
|
||||
@@ -290,6 +287,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────
|
||||
const emptyMessages: Record<PulseTab, string> = {
|
||||
search: "Search Pulse notes by author or text.",
|
||||
foryou:
|
||||
"No notes yet. Follow people and agents to build your personalized feed.",
|
||||
people: "No notes yet. Follow people to see their updates here.",
|
||||
@@ -346,25 +344,34 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
{/* Tab bar */}
|
||||
<div className="shrink-0 px-4 pt-11 sm:px-6">
|
||||
<div className="relative mx-auto flex w-full max-w-2xl items-center justify-center">
|
||||
<TabsList className="h-7 gap-0.5 rounded-full border border-border/50 bg-muted/40 p-0.5">
|
||||
<TabsTrigger value="foryou" className={tabTriggerClassName}>
|
||||
Everyone
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="people" className={tabTriggerClassName}>
|
||||
Following
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agents" className={tabTriggerClassName}>
|
||||
Agents
|
||||
{relayAgents.length > 0 ? (
|
||||
<span className="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-muted px-1 text-[10px] font-medium text-muted-foreground">
|
||||
{relayAgents.length}
|
||||
</span>
|
||||
) : null}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mine" className={tabTriggerClassName}>
|
||||
Mine
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex items-center">
|
||||
<TabsList className="h-7 gap-0.5 rounded-full border border-border/50 bg-muted/40 p-0.5">
|
||||
<TabsTrigger
|
||||
aria-label="Search Pulse"
|
||||
value="search"
|
||||
className="h-6 w-6 rounded-full p-0 !text-muted-foreground shadow-none transition-colors hover:!text-foreground data-[state=active]:bg-background data-[state=active]:!text-foreground data-[state=active]:shadow-sm dark:!text-white/35 dark:hover:!text-white/70 dark:data-[state=active]:!text-white"
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="foryou" className={tabTriggerClassName}>
|
||||
Everyone
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="people" className={tabTriggerClassName}>
|
||||
Following
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agents" className={tabTriggerClassName}>
|
||||
Agents
|
||||
{relayAgents.length > 0 ? (
|
||||
<span className="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-muted px-1 text-[10px] font-medium text-muted-foreground dark:text-white/45">
|
||||
{relayAgents.length}
|
||||
</span>
|
||||
) : null}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mine" className={tabTriggerClassName}>
|
||||
Mine
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-0 flex items-center gap-1">
|
||||
{activeTab === "agents" && relayAgents.length > 1 ? (
|
||||
@@ -375,17 +382,6 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
selectedPubkey={agentFilter}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
className="h-7 w-7"
|
||||
disabled={isRefetching}
|
||||
onClick={handleRefresh}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3.5 w-3.5 ${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,47 +393,77 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
className="mt-0 min-h-0 flex-1 overflow-y-auto"
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-2xl flex-col px-4 pb-10 pt-7 sm:px-6">
|
||||
{activeTab !== "agents" ? (
|
||||
<div className="mb-7 flex items-start gap-3">
|
||||
<div className="shrink-0 pt-1">
|
||||
<UserAvatar
|
||||
avatarUrl={currentProfile?.avatarUrl ?? null}
|
||||
displayName={currentDisplayName}
|
||||
{activeTab === "search" ? (
|
||||
<div className="flex min-h-[56vh] items-center justify-center">
|
||||
<div className="relative flex w-full max-w-xl flex-col items-center px-2">
|
||||
<div className="pointer-events-none absolute -inset-x-24 -inset-y-20 rounded-full bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.10),rgba(255,255,255,0)_56%)] opacity-80" />
|
||||
<img
|
||||
alt="Sprout"
|
||||
className="mb-7 h-16 w-16 object-contain opacity-95 dark:invert"
|
||||
src="/sprout.svg"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="truncate text-sm font-semibold leading-none">
|
||||
{currentDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
{publishMutation.isError && (
|
||||
<div className="mb-2 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{publishMutation.error instanceof Error
|
||||
? publishMutation.error.message
|
||||
: "Failed to publish note"}
|
||||
<div className="relative w-full max-w-lg">
|
||||
<div className="pointer-events-none absolute -inset-2 rounded-full bg-foreground/[0.03] blur-md dark:bg-white/[0.08]" />
|
||||
<div className="relative rounded-full border border-foreground/10 bg-background/80 p-1 shadow-[0_12px_48px_rgba(0,0,0,0.12)] backdrop-blur-xl dark:border-white/10 dark:bg-white/[0.04] dark:shadow-[0_16px_70px_rgba(0,0,0,0.55)]">
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground dark:text-white/55" />
|
||||
<Input
|
||||
autoFocus
|
||||
className="h-9 rounded-full border-0 bg-transparent pl-10 pr-12 text-sm shadow-none placeholder:text-muted-foreground/80 focus-visible:ring-0 dark:text-white dark:placeholder:text-white/60"
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
placeholder="What would you like to know?"
|
||||
type="search"
|
||||
value={searchQuery}
|
||||
/>
|
||||
<button
|
||||
aria-label="Search Pulse"
|
||||
className="absolute right-1.5 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-foreground/10 text-foreground transition-colors hover:bg-foreground/15 dark:bg-white/85 dark:text-black dark:hover:bg-white"
|
||||
type="button"
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<ForumComposer
|
||||
autocompleteBelow
|
||||
className="rounded-xl border-border/60 bg-muted/30 px-3 py-2 shadow-none"
|
||||
members={pulseMentionMembers}
|
||||
placeholder="What's on your mind?"
|
||||
isSending={publishMutation.isPending}
|
||||
onSubmit={(content, mentionPubkeys, mediaTags) =>
|
||||
publishMutation.mutateAsync({
|
||||
content,
|
||||
mentionPubkeys,
|
||||
mediaTags,
|
||||
})
|
||||
}
|
||||
profiles={mentionProfiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab !== "agents" ? (
|
||||
<div className="mb-7">
|
||||
{publishMutation.isError && (
|
||||
<div className="mb-2 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{publishMutation.error instanceof Error
|
||||
? publishMutation.error.message
|
||||
: "Failed to publish note"}
|
||||
</div>
|
||||
)}
|
||||
<ForumComposer
|
||||
autocompleteBelow
|
||||
className="pulse-composer rounded-xl border-border/60 bg-muted/30 p-2 shadow-none"
|
||||
header={
|
||||
<div className="pt-1">
|
||||
<UserAvatar
|
||||
avatarUrl={currentProfile?.avatarUrl ?? null}
|
||||
className="!h-7 !w-7 shrink-0"
|
||||
displayName={currentDisplayName}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
members={pulseMentionMembers}
|
||||
placeholder="What's on your mind?"
|
||||
isSending={publishMutation.isPending}
|
||||
onSubmit={(content, mentionPubkeys, mediaTags) =>
|
||||
publishMutation.mutateAsync({
|
||||
content,
|
||||
mentionPubkeys,
|
||||
mediaTags,
|
||||
})
|
||||
}
|
||||
profiles={mentionProfiles}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-1">{renderTimeline()}</div>
|
||||
{activeTab !== "search" ? (
|
||||
<div className="space-y-1">{renderTimeline()}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -22,6 +22,20 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.pulse-composer
|
||||
.rich-text-composer
|
||||
.tiptap
|
||||
p.is-editor-empty:first-child::before {
|
||||
color: hsl(var(--muted-foreground) / 0.65);
|
||||
}
|
||||
|
||||
.pulse-composer:focus-within
|
||||
.rich-text-composer
|
||||
.tiptap
|
||||
p.is-editor-empty:first-child::before {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user