diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 72fc1b62..e7a7752a 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -1,7 +1,9 @@ +import { useState } from "react"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { AudioNavigationGuard } from "@/components/AudioNavigationGuard"; import { DeepLinkHandler } from "@/components/DeepLinkHandler"; import { MinimizedAudioBar } from "@/components/MinimizedAudioBar"; +import { ReplyComposeModal } from "@/components/ReplyComposeModal"; import { AudioPlayerProvider } from "@/contexts/AudioPlayerContext"; import { sidebarItemIcon } from "@/lib/sidebarItems"; import { MainLayout } from "./components/MainLayout"; @@ -56,6 +58,22 @@ const decksDef = getExtraKindDef("decks")!; const emojisDef = getExtraKindDef("emojis")!; const developmentDef = getExtraKindDef("development")!; +/** Polls feed page with a FAB that opens the compose modal (poll mode via + menu). */ +function PollsFeedPage() { + const [composeOpen, setComposeOpen] = useState(false); + return ( + <> + setComposeOpen(true)} + /> + + + ); +} + /** Redirects /profile to the user's canonical profile URL (nip05 or npub). */ function ProfileRedirect() { const { user, metadata } = useCurrentUser(); @@ -111,16 +129,7 @@ export function AppRouter() { } /> } /> } /> - - } - /> + } /> } /> void; /** Pre-filled content for the compose box. */ initialContent?: string; + /** Open directly in poll mode. */ + initialMode?: 'post' | 'poll'; } /** Circular progress ring for character count. */ @@ -149,6 +157,7 @@ function CharRing({ count, max }: { count: number; max: number }) { ); } + export function ComposeBox({ onSuccess, placeholder = "What's on your mind?", @@ -160,11 +169,12 @@ export function ComposeBox({ previewMode: controlledPreviewMode, onHasPreviewableContentChange, initialContent = '', + initialMode = 'post', }: ComposeBoxProps) { const { user, metadata, isLoading: isProfileLoading } = useCurrentUser(); const avatarShape = getAvatarShape(metadata); const userProfileUrl = useProfileUrl(user?.pubkey ?? '', metadata); - const { mutateAsync: createEvent, isPending } = useNostrPublish(); + const { mutateAsync: createEvent, isPending, isPending: isPollPending } = useNostrPublish(); const { mutateAsync: postComment, isPending: isCommentPending } = usePostComment(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { feedSettings } = useFeedSettings(); @@ -180,7 +190,18 @@ export function ComposeBox({ const [cwText, setCwText] = useState(''); const [pickerOpen, setPickerOpen] = useState(false); const [pickerTab, setPickerTab] = useState<'emoji' | 'gif' | 'stickers'>('emoji'); + const [trayOpen, setTrayOpen] = useState(false); const [internalPreviewMode, setInternalPreviewMode] = useState(false); + + // Poll mode state + const [mode, setMode] = useState<'post' | 'poll'>(initialMode); + const [pollQuestion, setPollQuestion] = useState(''); + const [pollOptions, setPollOptions] = useState([ + { id: pollOptionId(), label: '' }, + { id: pollOptionId(), label: '' }, + ]); + const [pollType, setPollType] = useState<'singlechoice' | 'multiplechoice'>('singlechoice'); + const [pollDuration, setPollDuration] = useState<7 | 3 | 1 | 0>(7); const [removedEmbeds, setRemovedEmbeds] = useState>(new Set()); const [_uploadedFileTags, setUploadedFileTags] = useState([]); /** Maps uploaded file URLs to their NIP-94 tags (grouped per upload). */ @@ -953,6 +974,39 @@ export function ComposeBox({ } }; + const handlePollSubmit = async () => { + const filledOptions = pollOptions.filter((o) => o.label.trim()); + if (!pollQuestion.trim() || filledOptions.length < 2 || !user || isPollPending) return; + + const tags: string[][] = []; + for (const opt of filledOptions) { + tags.push(['option', opt.id, opt.label.trim()]); + } + tags.push(['polltype', pollType]); + if (pollDuration > 0) { + tags.push(['endsAt', String(Math.floor(Date.now() / 1000) + pollDuration * 86_400)]); + } + tags.push(['alt', `Poll: ${pollQuestion.trim()}`]); + + try { + await createEvent({ kind: 1068, content: pollQuestion.trim(), tags }); + // Reset poll state + setMode('post'); + setPollQuestion(''); + setPollOptions([{ id: pollOptionId(), label: '' }, { id: pollOptionId(), label: '' }]); + setPollType('singlechoice'); + setPollDuration(7); + queryClient.invalidateQueries({ queryKey: ['feed'] }); + toast({ title: 'Poll published!' }); + onSuccess?.(); + } catch { + toast({ title: 'Error', description: 'Failed to publish poll.', variant: 'destructive' }); + } + }; + + const pollFilledCount = pollOptions.filter((o) => o.label.trim()).length; + const isPollValid = pollQuestion.trim().length > 0 && pollFilledCount >= 2; + const isExpanded = forceExpanded || expanded || content.length > 0 || !compact; // Early return after all hooks to avoid violating Rules of Hooks @@ -1007,8 +1061,113 @@ export function ComposeBox({ )}
- {!previewMode ? ( - /* Edit mode - Textarea */ + {mode === 'poll' ? ( + /* ── Inline poll builder ─────────────────────────────── */ +
+ {/* Back to post link — hidden when poll mode is the only mode */} + {initialMode !== 'poll' && ( + + )} + + {/* Question */} +