import { useState, useRef, useEffect, useMemo } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { Layout } from '@/components/Layout'; 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 { 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 { 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: 'Group chat - SkateSpots' }); const { data: messages, isLoading, refetch, isRefetching } = useGroupConversation(allParticipants); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); if (!participantsParam || otherPubkeys.length === 0) { return (
This group chat link is invalid.
); } if (!user) { return (

Log in to view this group

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

Secure signer required

Group messages need a NIP-44 capable signer.

); } const handleSend = () => { if (!newMessage.trim() || isSending) return; sendMessage( { recipients: otherPubkeys, content: newMessage.trim() }, { onSuccess: () => setNewMessage(''), onError: (error) => { toast({ title: 'Message failed to send', description: error instanceof Error ? error.message : 'Please try again in a moment.', variant: 'destructive', }); }, }, ); }; return (
{/* Header */}

{otherPubkeys.length === 1 ? ( ) : ( `${otherPubkeys.length + 1} people` )}

End-to-end encrypted group

{/* 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 })}
); }) ) : (
No messages yet — say hi to the group.
)}
{/* Composer */}