mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
Refine Pulse composer and actions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,35 +16,13 @@ import { useRichTextEditor } from "@/features/messages/lib/useRichTextEditor";
|
||||
import { DropZoneOverlay } from "@/features/messages/ui/ComposerAttachments";
|
||||
import type { MentionSuggestion } from "@/features/messages/ui/MentionAutocomplete";
|
||||
import { MessageComposerToolbar } from "@/features/messages/ui/MessageComposerToolbar";
|
||||
import type { ChannelMember } from "@/shared/api/types";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import type { ForumComposerProps } from "./ForumComposer.types";
|
||||
import { ForumComposerAutocompletes } from "./ForumComposerAutocompletes";
|
||||
import { ForumComposerCompactLayout } from "./ForumComposerCompactLayout";
|
||||
import { ForumComposerMediaStatus } from "./ForumComposerMediaStatus";
|
||||
|
||||
type ForumComposerProps = {
|
||||
channelId?: string | null;
|
||||
/** Override mention source when no channel is available (e.g. Pulse). */
|
||||
members?: ChannelMember[];
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
disabled?: boolean;
|
||||
header?: React.ReactNode;
|
||||
isSending?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSubmit: (
|
||||
content: string,
|
||||
mentionPubkeys: string[],
|
||||
mediaTags?: string[][],
|
||||
) => undefined | Promise<unknown>;
|
||||
/** Render as a single-line composer until the user focuses it. */
|
||||
compact?: boolean;
|
||||
/** When true, autocomplete renders below the input (for top-of-view composers). */
|
||||
autocompleteBelow?: boolean;
|
||||
profiles?: UserProfileLookup;
|
||||
};
|
||||
import { useCompactComposerInteractions } from "./useCompactComposerInteractions";
|
||||
|
||||
export function ForumComposer({
|
||||
channelId = null,
|
||||
@@ -72,10 +50,19 @@ export function ForumComposer({
|
||||
if (pressed) setIsEmojiPickerOpen(false);
|
||||
setIsFormattingOpen(pressed);
|
||||
}, []);
|
||||
const expandCompactComposer = React.useCallback(() => {
|
||||
if (compact) setIsCompactExpanded(true);
|
||||
}, [compact]);
|
||||
|
||||
const mentions = useMentions(channelId, members, profiles);
|
||||
const channelLinks = useChannelLinks();
|
||||
const media = useMediaUpload();
|
||||
const { handlePaperclipClick, handleToolbarMouseDown, shouldIgnoreBlur } =
|
||||
useCompactComposerInteractions({
|
||||
compact,
|
||||
onExpand: expandCompactComposer,
|
||||
onPaperclip: media.handlePaperclip,
|
||||
});
|
||||
|
||||
const disabledRef = React.useRef(disabled);
|
||||
const isSendingRef = React.useRef(isSending);
|
||||
@@ -338,12 +325,6 @@ export function ForumComposer({
|
||||
(content.trim().length === 0 && media.pendingImeta.length === 0),
|
||||
[disabled, content, media.pendingImeta.length],
|
||||
);
|
||||
const handlePaperclipClick = React.useCallback(() => {
|
||||
void media.handlePaperclip();
|
||||
}, [media.handlePaperclip]);
|
||||
const expandCompactComposer = React.useCallback(() => {
|
||||
if (compact) setIsCompactExpanded(true);
|
||||
}, [compact]);
|
||||
const hasComposerContent =
|
||||
content.trim().length > 0 ||
|
||||
media.pendingImeta.length > 0 ||
|
||||
@@ -369,6 +350,9 @@ export function ForumComposer({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (shouldIgnoreBlur()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDraft =
|
||||
contentRef.current.trim().length > 0 ||
|
||||
@@ -387,6 +371,7 @@ export function ForumComposer({
|
||||
media.isUploading,
|
||||
media.pendingImetaRef,
|
||||
media.uploadState.status,
|
||||
shouldIgnoreBlur,
|
||||
],
|
||||
);
|
||||
const wasCompactExpandedRef = React.useRef(isCompactExpanded);
|
||||
@@ -484,7 +469,7 @@ export function ForumComposer({
|
||||
isFormattingOpen={isFormattingOpen}
|
||||
isSending={isSending ?? false}
|
||||
isUploading={media.isUploading}
|
||||
onCaptureSelection={() => {}}
|
||||
onCaptureSelection={handleToolbarMouseDown}
|
||||
onEmojiPickerOpenChange={setIsEmojiPickerOpen}
|
||||
onEmojiSelect={insertEmoji}
|
||||
onFormattingToggle={handleFormattingToggle}
|
||||
|
||||
26
desktop/src/features/forum/ui/ForumComposer.types.ts
Normal file
26
desktop/src/features/forum/ui/ForumComposer.types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type * as React from "react";
|
||||
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import type { ChannelMember } from "@/shared/api/types";
|
||||
|
||||
export type ForumComposerProps = {
|
||||
channelId?: string | null;
|
||||
/** Override mention source when no channel is available (e.g. Pulse). */
|
||||
members?: ChannelMember[];
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
disabled?: boolean;
|
||||
header?: React.ReactNode;
|
||||
isSending?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSubmit: (
|
||||
content: string,
|
||||
mentionPubkeys: string[],
|
||||
mediaTags?: string[][],
|
||||
) => undefined | Promise<unknown>;
|
||||
/** Render as a single-line composer until the user focuses it. */
|
||||
compact?: boolean;
|
||||
/** When true, autocomplete renders below the input (for top-of-view composers). */
|
||||
autocompleteBelow?: boolean;
|
||||
profiles?: UserProfileLookup;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as React from "react";
|
||||
|
||||
import { EditorContent, type Editor } from "@tiptap/react";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -35,11 +35,14 @@ export function ForumComposerCompactLayout({
|
||||
</div>
|
||||
<Button
|
||||
aria-label={isSending ? "Sending" : "Send message"}
|
||||
className={cn("h-9 w-9 shrink-0 rounded-full")}
|
||||
className={cn(
|
||||
"h-7 w-7 shrink-0 rounded-full border border-border/70 bg-transparent p-0 text-muted-foreground shadow-none hover:bg-transparent hover:text-foreground",
|
||||
)}
|
||||
data-testid="send-message"
|
||||
disabled={sendDisabled || isSending}
|
||||
size="icon"
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
>
|
||||
{isSending ? (
|
||||
<span
|
||||
@@ -47,7 +50,7 @@ export function ForumComposerCompactLayout({
|
||||
className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
|
||||
/>
|
||||
) : (
|
||||
<ArrowUp aria-hidden className="h-3.5 w-3.5" />
|
||||
<Plus aria-hidden className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from "react";
|
||||
|
||||
type UseCompactComposerInteractionsArgs = {
|
||||
compact: boolean;
|
||||
onExpand: () => void;
|
||||
onPaperclip: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
export function useCompactComposerInteractions({
|
||||
compact,
|
||||
onExpand,
|
||||
onPaperclip,
|
||||
}: UseCompactComposerInteractionsArgs) {
|
||||
const isMediaPickerActiveRef = React.useRef(false);
|
||||
const isToolbarInteractionActiveRef = React.useRef(false);
|
||||
|
||||
const handlePaperclipClick = React.useCallback(() => {
|
||||
if (compact) onExpand();
|
||||
isMediaPickerActiveRef.current = true;
|
||||
void onPaperclip().finally(() => {
|
||||
isMediaPickerActiveRef.current = false;
|
||||
});
|
||||
}, [compact, onExpand, onPaperclip]);
|
||||
|
||||
const handleToolbarMouseDown = React.useCallback(() => {
|
||||
if (compact) onExpand();
|
||||
isToolbarInteractionActiveRef.current = true;
|
||||
window.setTimeout(() => {
|
||||
isToolbarInteractionActiveRef.current = false;
|
||||
}, 0);
|
||||
}, [compact, onExpand]);
|
||||
|
||||
const shouldIgnoreBlur = React.useCallback(
|
||||
() =>
|
||||
isMediaPickerActiveRef.current || isToolbarInteractionActiveRef.current,
|
||||
[],
|
||||
);
|
||||
|
||||
return { handlePaperclipClick, handleToolbarMouseDown, shouldIgnoreBlur };
|
||||
}
|
||||
@@ -90,6 +90,7 @@ export const MessageComposerToolbar = React.memo(
|
||||
aria-pressed={isFormattingOpen}
|
||||
disabled={composerDisabled}
|
||||
onClick={() => onFormattingToggle(!isFormattingOpen)}
|
||||
onMouseDown={onCaptureSelection}
|
||||
className={cn(
|
||||
"inline-flex h-8 min-w-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-colors",
|
||||
"hover:bg-muted hover:text-foreground",
|
||||
@@ -120,6 +121,7 @@ export const MessageComposerToolbar = React.memo(
|
||||
aria-label="Close formatting"
|
||||
disabled={composerDisabled}
|
||||
onClick={() => onFormattingToggle(false)}
|
||||
onMouseDown={onCaptureSelection}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -180,6 +182,7 @@ export const MessageComposerToolbar = React.memo(
|
||||
aria-label="Attach image"
|
||||
disabled={composerDisabled || isUploading}
|
||||
onClick={onPaperclip}
|
||||
onMouseDown={onCaptureSelection}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -214,6 +217,7 @@ export const MessageComposerToolbar = React.memo(
|
||||
aria-pressed={isFormattingOpen}
|
||||
disabled={composerDisabled}
|
||||
onClick={() => onFormattingToggle(!isFormattingOpen)}
|
||||
onMouseDown={onCaptureSelection}
|
||||
className={cn(
|
||||
"inline-flex h-8 min-w-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-colors",
|
||||
"hover:bg-muted hover:text-foreground",
|
||||
|
||||
@@ -51,7 +51,7 @@ export function AgentActivityCard({
|
||||
const summaryNote = group.notes[0];
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl px-1 py-4 transition-colors hover:bg-muted/20 sm:px-2">
|
||||
<div className="rounded-2xl px-1 py-4 sm:px-2">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative shrink-0 pt-1">
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import {
|
||||
ALargeSmall,
|
||||
AtSign,
|
||||
Bookmark,
|
||||
Bot,
|
||||
MessageCircle,
|
||||
Share2,
|
||||
PenSquare,
|
||||
Paperclip,
|
||||
SmilePlus,
|
||||
UserMinus,
|
||||
UserPlus,
|
||||
SquareArrowOutUpRight,
|
||||
ThumbsUp,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import type { UserNote } from "@/shared/api/socialTypes";
|
||||
import type { UserProfileSummary } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
|
||||
type NoteCardProps = {
|
||||
note: UserNote;
|
||||
profile?: UserProfileSummary | null;
|
||||
currentUserDisplayName?: string;
|
||||
currentUserProfile?: UserProfileSummary | null;
|
||||
isAgent?: boolean;
|
||||
isOwnNote: boolean;
|
||||
isFollowing: boolean;
|
||||
onFollow?: (pubkey: string) => void;
|
||||
onReact?: (note: UserNote) => void;
|
||||
onReply?: (note: UserNote) => void;
|
||||
onShare?: (note: UserNote) => void;
|
||||
onUnfollow?: (pubkey: string) => void;
|
||||
@@ -44,20 +49,43 @@ function formatRelativeTime(unixSeconds: number): string {
|
||||
export function NoteCard({
|
||||
note,
|
||||
profile,
|
||||
currentUserDisplayName = "You",
|
||||
currentUserProfile,
|
||||
isAgent,
|
||||
isOwnNote,
|
||||
isFollowing,
|
||||
onFollow,
|
||||
onReact,
|
||||
onReply,
|
||||
onShare,
|
||||
onUnfollow,
|
||||
}: NoteCardProps) {
|
||||
const displayName = profile?.displayName ?? `${note.pubkey.slice(0, 8)}...`;
|
||||
const avatarUrl = profile?.avatarUrl ?? null;
|
||||
const [isUpvoted, setIsUpvoted] = React.useState(false);
|
||||
const [isBookmarked, setIsBookmarked] = React.useState(false);
|
||||
const [isReplyComposerOpen, setIsReplyComposerOpen] = React.useState(false);
|
||||
const [replyDraft, setReplyDraft] = React.useState("");
|
||||
const replyInputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
const actionButtonClass =
|
||||
"inline-flex min-w-7 items-center gap-1.5 text-muted-foreground/60 transition-colors hover:text-foreground focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
||||
const activeActionClass = "text-foreground";
|
||||
const countPlaceholder = <span aria-hidden className="w-2.5" />;
|
||||
const currentUserAvatarUrl = currentUserProfile?.avatarUrl ?? null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isReplyComposerOpen) return;
|
||||
replyInputRef.current?.focus();
|
||||
}, [isReplyComposerOpen]);
|
||||
|
||||
const handleReplySubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (replyDraft.trim().length === 0) return;
|
||||
setReplyDraft("");
|
||||
setIsReplyComposerOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<article className="group flex items-start gap-2.5 rounded-2xl px-1 pb-1 pt-4 transition-colors hover:bg-muted/20 sm:px-2">
|
||||
<article className="flex items-start gap-2.5 rounded-2xl px-1 pb-1 pt-4 sm:px-2">
|
||||
<div className="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatarUrl={avatarUrl}
|
||||
@@ -69,7 +97,7 @@ export function NoteCard({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<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}
|
||||
@@ -87,82 +115,159 @@ export function NoteCard({
|
||||
<span className="shrink-0 text-xs text-muted-foreground/70">
|
||||
{formatRelativeTime(note.createdAt)}
|
||||
</span>
|
||||
{!isOwnNote ? (
|
||||
<div className="ml-auto hidden shrink-0 group-hover:block">
|
||||
{isFollowing ? (
|
||||
<Button
|
||||
className="h-6 gap-1 px-2 text-xs text-muted-foreground"
|
||||
onClick={() => onUnfollow?.(note.pubkey)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<UserMinus className="h-3 w-3" />
|
||||
Unfollow
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="h-6 gap-1 px-2 text-xs text-muted-foreground"
|
||||
onClick={() => onFollow?.(note.pubkey)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<UserPlus className="h-3 w-3" />
|
||||
Follow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-0.5 pb-3 text-sm text-foreground">
|
||||
<Markdown content={note.content} tight />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-[11px] font-medium text-muted-foreground">
|
||||
<div className="flex flex-wrap items-center gap-5 text-xs font-medium">
|
||||
<div className="flex flex-wrap items-center gap-5">
|
||||
<button
|
||||
aria-label={isUpvoted ? "Remove upvote" : "Upvote"}
|
||||
aria-pressed={isUpvoted}
|
||||
className={`${actionButtonClass} ${isUpvoted ? activeActionClass : ""}`}
|
||||
onClick={() => setIsUpvoted((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<ThumbsUp
|
||||
className={`h-4 w-4 ${isUpvoted ? "fill-current" : ""}`}
|
||||
/>
|
||||
{countPlaceholder}
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reply"
|
||||
aria-expanded={isReplyComposerOpen}
|
||||
className={actionButtonClass}
|
||||
onClick={() => {
|
||||
setIsReplyComposerOpen(true);
|
||||
onReply?.(note);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
{countPlaceholder}
|
||||
</button>
|
||||
<button
|
||||
aria-label="Share"
|
||||
className={actionButtonClass}
|
||||
onClick={() => onShare?.(note)}
|
||||
type="button"
|
||||
>
|
||||
<SquareArrowOutUpRight className="h-4 w-4" />
|
||||
{countPlaceholder}
|
||||
</button>
|
||||
<button
|
||||
aria-label="Start direct message"
|
||||
className={actionButtonClass}
|
||||
type="button"
|
||||
>
|
||||
<PenSquare className="h-4 w-4" />
|
||||
</button>
|
||||
{!isOwnNote ? (
|
||||
isFollowing ? (
|
||||
<button
|
||||
className="text-muted-foreground/60 transition-colors hover:text-foreground hover:underline focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() => onUnfollow?.(note.pubkey)}
|
||||
type="button"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="text-muted-foreground/60 transition-colors hover:text-foreground hover:underline focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() => onFollow?.(note.pubkey)}
|
||||
type="button"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
className="inline-flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => onReact?.(note)}
|
||||
aria-label={isBookmarked ? "Remove bookmark" : "Save"}
|
||||
aria-pressed={isBookmarked}
|
||||
className={`${actionButtonClass} ${isBookmarked ? activeActionClass : ""}`}
|
||||
onClick={() => setIsBookmarked((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<SmilePlus className="h-3.5 w-3.5" />
|
||||
React
|
||||
<Bookmark
|
||||
className={`h-4 w-4 ${isBookmarked ? "fill-current" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="inline-flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => onReply?.(note)}
|
||||
type="button"
|
||||
>
|
||||
<MessageCircle className="h-3.5 w-3.5" />
|
||||
Reply
|
||||
</button>
|
||||
<button
|
||||
className="inline-flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => onShare?.(note)}
|
||||
type="button"
|
||||
>
|
||||
<Share2 className="h-3.5 w-3.5" />
|
||||
Share
|
||||
</button>
|
||||
{!isOwnNote ? (
|
||||
isFollowing ? (
|
||||
<button
|
||||
className="hover:text-foreground"
|
||||
onClick={() => onUnfollow?.(note.pubkey)}
|
||||
type="button"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="hover:text-foreground"
|
||||
onClick={() => onFollow?.(note.pubkey)}
|
||||
type="button"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
{isReplyComposerOpen ? (
|
||||
<form
|
||||
className="mt-4 flex gap-2 rounded-2xl border border-border/60 bg-background/60 p-3"
|
||||
onSubmit={handleReplySubmit}
|
||||
>
|
||||
<UserAvatar
|
||||
avatarUrl={currentUserAvatarUrl}
|
||||
className="!h-8 !w-8 shrink-0"
|
||||
displayName={currentUserDisplayName}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<textarea
|
||||
className="min-h-16 w-full resize-none bg-transparent text-sm text-foreground placeholder:text-muted-foreground/70 focus:outline-none"
|
||||
onChange={(event) => setReplyDraft(event.target.value)}
|
||||
placeholder="Post your reply"
|
||||
ref={replyInputRef}
|
||||
value={replyDraft}
|
||||
/>
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<button
|
||||
aria-label="Mention someone"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
>
|
||||
<AtSign className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Attach media"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Add emoji"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
>
|
||||
<SmilePlus className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Formatting"
|
||||
className="inline-flex h-8 min-w-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
>
|
||||
<ALargeSmall className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="text-xs font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() => {
|
||||
setReplyDraft("");
|
||||
setIsReplyComposerOpen(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="rounded-full bg-foreground px-3 py-1 text-xs font-semibold text-background transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={replyDraft.trim().length === 0}
|
||||
type="submit"
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -321,6 +321,8 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
) : (
|
||||
visibleNotes.map((note) => (
|
||||
<NoteCard
|
||||
currentUserDisplayName={currentDisplayName}
|
||||
currentUserProfile={currentProfile}
|
||||
isAgent={agentPubkeySet.has(note.pubkey)}
|
||||
isFollowing={followingSet.has(note.pubkey)}
|
||||
isOwnNote={note.pubkey === currentPubkey}
|
||||
@@ -342,7 +344,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
{/* Tab bar */}
|
||||
<div className="shrink-0 px-4 pt-11 sm:px-6">
|
||||
<div className="relative z-40 shrink-0 px-4 pt-2 sm:px-6">
|
||||
<div className="relative mx-auto flex w-full max-w-2xl items-center justify-center">
|
||||
<div className="flex items-center">
|
||||
<TabsList className="h-8 gap-0.5 rounded-full border border-border/50 bg-muted/40 p-0.5">
|
||||
@@ -392,12 +394,16 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
forceMount
|
||||
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">
|
||||
<div
|
||||
className={`mx-auto flex w-full max-w-2xl flex-col px-4 pb-10 sm:px-6 ${
|
||||
activeTab !== "search" && activeTab !== "agents" ? "pt-0" : "pt-7"
|
||||
}`}
|
||||
>
|
||||
{activeTab === "search" ? (
|
||||
<div className="flex min-h-[56vh] items-center justify-center">
|
||||
<div className="flex min-h-[calc(100vh-96px)] items-center justify-center">
|
||||
<div className="relative flex w-full max-w-xl flex-col items-center px-2">
|
||||
<h2 className="mb-5 text-center text-2xl font-semibold tracking-tight text-foreground">
|
||||
Search Pulse
|
||||
What are you looking for?
|
||||
</h2>
|
||||
<div className="relative w-full max-w-lg">
|
||||
<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)]">
|
||||
@@ -422,7 +428,11 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab !== "agents" ? (
|
||||
<div className="mb-7">
|
||||
<div className="sticky top-0 z-10 mb-7 pb-3 pt-7">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-[-1px] h-8 bg-background"
|
||||
/>
|
||||
{publishMutation.isError && (
|
||||
<div className="mb-2 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{publishMutation.error instanceof Error
|
||||
@@ -432,7 +442,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
)}
|
||||
<ForumComposer
|
||||
autocompleteBelow
|
||||
className="pulse-composer rounded-xl border-border/60 bg-muted/30 p-2 shadow-none"
|
||||
className="pulse-composer overflow-hidden rounded-2xl border-border/50 bg-background/70 p-2 shadow-none backdrop-blur-xl supports-[backdrop-filter]:bg-background/55"
|
||||
compact
|
||||
header={
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
@@ -462,7 +472,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
|
||||
) : null}
|
||||
|
||||
{activeTab !== "search" ? (
|
||||
<div className="space-y-1">{renderTimeline()}</div>
|
||||
<div className="space-y-4">{renderTimeline()}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user