From d3d6d56e4996bde0d0bb306feac8434df3dfd9bd Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 19 Feb 2026 06:52:45 -0600 Subject: [PATCH] Move preview toggle to top of compose modal - Moved Edit/Preview toggle from toolbar to modal header - Added controlled preview mode to ComposeBox component - Toggle now appears next to close button in modal header - Added border-bottom to header for visual separation - ComposeBox can be controlled externally or use internal state - Fixes layout issues and improves UX in modal Co-authored-by: shakespeare.diy --- src/components/ComposeBox.tsx | 39 +++++++++++++++++---- src/components/ReplyComposeModal.tsx | 52 +++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/components/ComposeBox.tsx b/src/components/ComposeBox.tsx index ce890160..d6d7ae28 100644 --- a/src/components/ComposeBox.tsx +++ b/src/components/ComposeBox.tsx @@ -34,6 +34,10 @@ interface ComposeBoxProps { forceExpanded?: boolean; /** If true, hides the avatar (useful inside modals with their own layout). */ hideAvatar?: boolean; + /** Controlled preview mode (for modal usage). */ + previewMode?: boolean; + /** Callback to notify parent of previewable content changes. */ + onHasPreviewableContentChange?: (hasContent: boolean) => void; } /** Circular progress ring for character count. */ @@ -73,7 +77,17 @@ function CharRing({ count, max }: { count: number; max: number }) { ); } -export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", compact = false, replyTo, quotedEvent, forceExpanded = false, hideAvatar = false }: ComposeBoxProps) { +export function ComposeBox({ + onSuccess, + placeholder = "What's on your mind?", + compact = false, + replyTo, + quotedEvent, + forceExpanded = false, + hideAvatar = false, + previewMode: controlledPreviewMode, + onHasPreviewableContentChange, +}: ComposeBoxProps) { const { user, metadata } = useCurrentUser(); const { mutateAsync: createEvent, isPending } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); @@ -85,12 +99,15 @@ export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", co const [cwEnabled, setCwEnabled] = useState(false); const [cwText, setCwText] = useState(''); const [emojiOpen, setEmojiOpen] = useState(false); - const [previewMode, setPreviewMode] = useState(false); + const [internalPreviewMode, setInternalPreviewMode] = useState(false); const [removedEmbeds, setRemovedEmbeds] = useState>(new Set()); const [uploadedFileTags, setUploadedFileTags] = useState([]); const textareaRef = useRef(null); const fileInputRef = useRef(null); + // Use controlled preview mode if provided, otherwise use internal state + const previewMode = controlledPreviewMode !== undefined ? controlledPreviewMode : internalPreviewMode; + // Auto-expand when quotedEvent is provided useEffect(() => { if (quotedEvent) { @@ -215,6 +232,13 @@ export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", co return visibleEmbeds.length > 0 || previewImages.length > 0 || previewVideos.length > 0; }, [visibleEmbeds, previewImages, previewVideos]); + // Notify parent of previewable content changes + useEffect(() => { + if (onHasPreviewableContentChange) { + onHasPreviewableContentChange(hasPreviewableContent); + } + }, [hasPreviewableContent, onHasPreviewableContentChange]); + // Include quoted event if provided and not removed const quotedEventId = quotedEvent ? nip19.neventEncode({ id: quotedEvent.id, author: quotedEvent.pubkey }) : null; const quotedEventKey = quotedEventId ? `nostr:${quotedEventId}` : null; @@ -593,11 +617,11 @@ export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", co - {/* Center: Preview toggle */} - {hasPreviewableContent && ( + {/* Center: Preview toggle (only show if not controlled by parent) */} + {hasPreviewableContent && controlledPreviewMode === undefined && (
)} + {/* Spacer when preview toggle is in modal */} + {hasPreviewableContent && controlledPreviewMode !== undefined &&
} + {/* Right: char count + post button */}
{charCount > 0 && ( diff --git a/src/components/ReplyComposeModal.tsx b/src/components/ReplyComposeModal.tsx index 5ae395a8..d0ff2797 100644 --- a/src/components/ReplyComposeModal.tsx +++ b/src/components/ReplyComposeModal.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { X } from 'lucide-react'; import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; @@ -15,6 +15,7 @@ import { ComposeBox } from '@/components/ComposeBox'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { timeAgo } from '@/lib/timeAgo'; +import { cn } from '@/lib/utils'; interface ReplyComposeModalProps { /** The event being replied to. When `null`, the modal acts as a "New post" composer. */ @@ -34,21 +35,54 @@ function extractImages(content: string): string[] { export function ReplyComposeModal({ event, quotedEvent, open, onOpenChange }: ReplyComposeModalProps) { const isReply = !!event; const isQuote = !!quotedEvent; + const [previewMode, setPreviewMode] = useState(false); + const [hasPreviewableContent, setHasPreviewableContent] = useState(false); return ( {/* Header */} -
+
{isReply ? 'Reply to post' : isQuote ? 'Quote post' : 'New post'} - + +
+ {/* Preview toggle */} + {hasPreviewableContent && ( +
+ + +
+ )} + + +
{/* Embedded original post (reply only - quote embeds are shown in ComposeBox) */} @@ -62,6 +96,8 @@ export function ReplyComposeModal({ event, quotedEvent, open, onOpenChange }: Re placeholder={isReply ? "What's on your mind?" : isQuote ? "Add a comment..." : "What's happening?"} forceExpanded hideAvatar + previewMode={previewMode} + onHasPreviewableContentChange={setHasPreviewableContent} />