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 'date-fns'; 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')} refetch()} disabled={isRefetching} className="text-gray-400 hover:text-white" > {/* 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 */} setNewMessage(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }} placeholder={t('socialUi.groupConversation.placeholder')} className="min-h-[44px] max-h-24 resize-none border-gray-700 bg-gray-900 text-sm text-white sm:max-h-32 sm:text-base" rows={1} /> {isSending ? : } > ); }
{t('socialUi.groupConversation.secureSignerDesc')}
{otherPubkeys.length === 1 ? ( ) : ( `${t('socialUi.dms.peopleCount', { count: otherPubkeys.length + 1 })}` )}
{t('socialUi.groupConversation.encryptedGroup')}