import { useState, useRef, useEffect, useMemo } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { LoginArea } from '@/components/auth/LoginArea'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Skeleton } from '@/components/ui/skeleton'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useTranslation } from '@/contexts/language'; import { useToast } from '@/hooks/useToast'; import { useNostrLogin } from '@nostrify/react/login'; import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities'; import { useGroupConversation, useSendDirectMessage } from '@/hooks/useDirectMessages'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { formatDistanceToNow } from '@/lib/dateFnsI18n'; import { ArrowLeft, Lock, Loader2, RefreshCw, Send, Users } from 'lucide-react'; import { cn } from '@/lib/utils'; function ParticipantName({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const name = author.data?.metadata?.name ?? genUserName(pubkey); return {name}; } /** * Group DM thread (NIP-17 fan-out group messaging). * * The URL carries the OTHER participants' hex pubkeys (comma-separated); * `useGroupConversation` takes the full set including us. This is the same * gift-wrap primitive as 1:1 DMs with one `p` tag per recipient. */ export default function GroupConversationPage() { const { t } = useTranslation(); const { participants: participantsParam } = useParams<{ participants: string }>(); const { user } = useCurrentUser(); const { logins } = useNostrLogin(); const { toast } = useToast(); const { mutate: sendMessage, isPending: isSending } = useSendDirectMessage(); const [newMessage, setNewMessage] = useState(''); const messagesEndRef = useRef(null); const otherPubkeys = useMemo( () => (participantsParam ? participantsParam.split(',').filter(Boolean) : []), [participantsParam], ); const allParticipants = useMemo( () => (user ? [user.pubkey, ...otherPubkeys] : otherPubkeys), [user, otherPubkeys], ); useSeoMeta({ title: t('socialUi.groupConversation.seoTitle') }); const { data: messages, isLoading, refetch, isRefetching } = useGroupConversation(allParticipants); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); if (!participantsParam || otherPubkeys.length === 0) { return ( <>
{t('socialUi.groupConversation.invalidLink')}
); } if (!user) { return ( <>

{t('socialUi.groupConversation.loginTitle')}

); } if (!checkNip44(user, logins)) { return ( <>

{t('socialUi.groupConversation.secureSignerTitle')}

{t('socialUi.groupConversation.secureSignerDesc')}

); } const handleSend = () => { if (!newMessage.trim() || isSending) return; sendMessage( { recipients: otherPubkeys, content: newMessage.trim() }, { onSuccess: () => setNewMessage(''), onError: (error) => { toast({ title: t('socialUi.conversation.sendFailed'), description: error instanceof Error ? error.message : t('socialUi.conversation.sendFailedDesc'), variant: 'destructive', }); }, }, ); }; return ( <>
{/* Header */}

{otherPubkeys.length === 1 ? ( ) : ( `${t('socialUi.dms.peopleCount', { count: otherPubkeys.length + 1 })}` )}

{t('socialUi.groupConversation.encryptedGroup')}

{/* Messages */}
{isLoading ? ( <> ) : messages && messages.length > 0 ? ( messages.map((msg) => { const isMine = msg.pubkey === user.pubkey; return (
{!isMine && ( )}
{msg.content}
{formatDistanceToNow(new Date(msg.createdAt * 1000), { addSuffix: true })}
); }) ) : (
{t('socialUi.groupConversation.empty')}
)}
{/* Composer */}