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 <assistant@shakespeare.diy>
This commit is contained in:
@@ -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<Set<string>>(new Set());
|
||||
const [uploadedFileTags, setUploadedFileTags] = useState<string[][]>([]);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Center: Preview toggle */}
|
||||
{hasPreviewableContent && (
|
||||
{/* Center: Preview toggle (only show if not controlled by parent) */}
|
||||
{hasPreviewableContent && controlledPreviewMode === undefined && (
|
||||
<div className="inline-flex items-center gap-0.5 p-1 bg-muted/50 rounded-lg mx-2">
|
||||
<button
|
||||
onClick={() => setPreviewMode(false)}
|
||||
onClick={() => setInternalPreviewMode(false)}
|
||||
className={cn(
|
||||
"px-3.5 py-1.5 text-xs font-medium rounded-md transition-all",
|
||||
!previewMode
|
||||
@@ -608,7 +632,7 @@ export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", co
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPreviewMode(true)}
|
||||
onClick={() => setInternalPreviewMode(true)}
|
||||
className={cn(
|
||||
"px-3.5 py-1.5 text-xs font-medium rounded-md transition-all",
|
||||
previewMode
|
||||
@@ -621,6 +645,9 @@ export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", co
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spacer when preview toggle is in modal */}
|
||||
{hasPreviewableContent && controlledPreviewMode !== undefined && <div className="flex-1" />}
|
||||
|
||||
{/* Right: char count + post button */}
|
||||
<div className="flex items-center gap-3">
|
||||
{charCount > 0 && (
|
||||
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[520px] rounded-2xl p-0 gap-0 border-border overflow-hidden [&>button]:hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 h-12">
|
||||
<div className="flex items-center justify-between px-4 h-12 border-b border-border">
|
||||
<DialogTitle className="text-base font-semibold">
|
||||
{isReply ? 'Reply to post' : isQuote ? 'Quote post' : 'New post'}
|
||||
</DialogTitle>
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="p-1.5 -mr-1.5 rounded-full text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Preview toggle */}
|
||||
{hasPreviewableContent && (
|
||||
<div className="inline-flex items-center gap-0.5 p-1 bg-muted/50 rounded-lg">
|
||||
<button
|
||||
onClick={() => setPreviewMode(false)}
|
||||
className={cn(
|
||||
"px-3.5 py-1.5 text-xs font-medium rounded-md transition-all",
|
||||
!previewMode
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPreviewMode(true)}
|
||||
className={cn(
|
||||
"px-3.5 py-1.5 text-xs font-medium rounded-md transition-all",
|
||||
previewMode
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="p-1.5 -mr-1.5 rounded-full text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user