diff --git a/src/components/ComposeBox.tsx b/src/components/ComposeBox.tsx index b2d4aa4f..8fecae0e 100644 --- a/src/components/ComposeBox.tsx +++ b/src/components/ComposeBox.tsx @@ -1,6 +1,6 @@ import { lazy, Suspense, useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { Link } from 'react-router-dom'; -import { Paperclip, Smile, AlertTriangle, X, Loader2, Mic, Square, Sticker, BarChart3, Plus, ChevronLeft } from 'lucide-react'; +import { Paperclip, Smile, AlertTriangle, X, Loader2, Mic, Square, Sticker, BarChart3, Plus, ChevronLeft, HelpCircle } from 'lucide-react'; import { nip19 } from 'nostr-tools'; import { encode as blurhashEncode } from 'blurhash'; import type { NostrEvent } from '@nostrify/nostrify'; @@ -12,6 +12,7 @@ import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'; import { useCustomEmojis } from '@/hooks/useCustomEmojis'; import { useFeedSettings } from '@/hooks/useFeedSettings'; import { GifPicker } from '@/components/GifPicker'; @@ -27,6 +28,9 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { usePostComment } from '@/hooks/usePostComment'; import { useUploadFile } from '@/hooks/useUploadFile'; +import { useCountryFollows } from '@/hooks/useCountryFollows'; +import { getCountryInfo } from '@/lib/countries'; +import { createCountryIdentifier } from '@/lib/countryIdentifiers'; import { useQueryClient } from '@tanstack/react-query'; import { useToast } from '@/hooks/useToast'; import { useAppContext } from '@/hooks/useAppContext'; @@ -249,6 +253,28 @@ export function ComposeBox({ // Poll mode state const [mode, setMode] = useState<'post' | 'poll'>(initialMode); + + // Country-destination toggle. Only meaningful for top-level new posts + // from the home feed (no replyTo, not a custom-kind publish). When a + // country code is selected, the post is published as a NIP-22 kind + // 1111 comment rooted on that country instead of a plain kind 1 note. + // Dropdown lists only the countries the user follows, with "Global" + // always at the top. + const { followedCountries } = useCountryFollows(); + const canChooseDestination = + !replyTo && !customPublish && mode === 'post' && !!user && followedCountries.length > 0; + /** `'world'` for a regular kind-1 note, or an ISO 3166 country code for a kind-1111 community post. */ + const [destination, setDestination] = useState<'world' | string>('world'); + const selectedCountryCode = destination !== 'world' ? destination : null; + const selectedCountryInfo = selectedCountryCode ? getCountryInfo(selectedCountryCode) : null; + // If the user unfollows the currently-selected country mid-session, + // snap back to world so we don't try to publish a kind 1111 with + // a root the user no longer cares about. + useEffect(() => { + if (selectedCountryCode && !followedCountries.includes(selectedCountryCode)) { + setDestination('world'); + } + }, [selectedCountryCode, followedCountries]); const [pollOptions, setPollOptions] = useState([ { id: pollOptionId(), label: '' }, { id: pollOptionId(), label: '' }, @@ -286,6 +312,7 @@ export function ComposeBox({ setUploadedFileGroups(new Map()); setWebxdcUuids(new Map()); setWebxdcMetas(new Map()); + setDestination('world'); // Clear the auto-saved draft try { localStorage.removeItem(draftKey); } catch { /* ignore */ } }, [initialMode, draftKey]); @@ -1069,6 +1096,12 @@ export function ComposeBox({ } await postComment({ root, reply, content: finalContent, tags }); + } else if (canChooseDestination && selectedCountryCode) { + // No replyTo, but the user picked a followed country as the + // destination — publish as a NIP-22 kind 1111 rooted on the + // country identifier (mirrors the country page's compose flow). + const countryRoot = new URL(createCountryIdentifier(selectedCountryCode)); + await postComment({ root: countryRoot, reply: undefined, content: finalContent, tags }); } else { await createEvent({ kind: 1, @@ -1245,7 +1278,13 @@ export function ComposeBox({ onPointerDown={expand} onFocus={expand} onPaste={handlePaste} - placeholder={mode === 'poll' ? 'Ask a question…' : placeholder} + placeholder={ + mode === 'poll' + ? 'Ask a question…' + : selectedCountryInfo + ? `What's happening in ${selectedCountryInfo.name}?` + : placeholder + } className={cn( 'w-full bg-transparent text-foreground placeholder:text-muted-foreground resize-none outline-none text-lg pt-2.5 pb-2 opacity-85 break-words overflow-hidden transition-[min-height] duration-200 ease-in-out', isExpanded ? 'min-h-[100px]' : 'min-h-[44px]', @@ -1419,6 +1458,114 @@ export function ComposeBox({ {/* end flex-1 content column */} {/* end avatar + content row */} + {/* Destination row — its own line above the toolbar so the + "Post to" label gives it semantic context. The dropdown lists + "Global" first, then every country the user follows. The + help icon opens a popover that explains the difference + between a global post and a country community post. Shown + only for new top-level posts from a logged-in user with + at least one followed country. */} + {canChooseDestination && isExpanded && ( +
Global
++ A regular post visible to everyone on Nostr. Anyone, anywhere can see it. +
+Your country community
++ Shown in that country's local feed alongside posts from neighbors. Best for community-relevant updates. +
+