import { useEffect, useMemo, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { formatDistanceToNow } from 'date-fns'; import { Lock, Send, Loader2, ShieldCheck, MessageSquare } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useToast } from '@/hooks/useToast'; import { useGroupConversation, useSendDirectMessage, type DirectMessage, } from '@/hooks/useDirectMessages'; import { genUserName } from '@/lib/genUserName'; import { cn } from '@/lib/utils'; interface CrewChatProps { crewName: string; /** The crew founder pubkey. */ founderPubkey: string; /** All listed member pubkeys (hex). */ members: string[]; } /** One message bubble, resolving the author's profile. */ function CrewMessageBubble({ message, isOwn, }: { message: DirectMessage; isOwn: boolean; }) { const author = useAuthor(message.pubkey); const meta = author.data?.metadata; const name = meta?.name ?? genUserName(message.pubkey); const npub = useMemo(() => nip19.npubEncode(message.pubkey), [message.pubkey]); const timeAgo = formatDistanceToNow(new Date(message.createdAt * 1000), { addSuffix: true }); return (
{name.slice(0, 2).toUpperCase()}
{name} {timeAgo}
{message.content}
); } export function CrewChat({ crewName, founderPubkey, members }: CrewChatProps) { const { user } = useCurrentUser(); const { toast } = useToast(); const [draft, setDraft] = useState(''); const scrollRef = useRef(null); // The chat participants = founder + all listed members (deduped). const participants = useMemo( () => Array.from(new Set([founderPubkey, ...members])), [founderPubkey, members], ); // Only crew members (founder or a listed member) can read/write the chat, // because NIP-17 group messages are end-to-end encrypted to the participants. const isMember = !!user && participants.includes(user.pubkey); const { data: messages, isLoading } = useGroupConversation(isMember ? participants : []); const { mutateAsync: sendMessage, isPending } = useSendDirectMessage(); // Auto-scroll to newest message when the list grows. const lastCount = useRef(0); useEffect(() => { const count = messages?.length ?? 0; if (count !== lastCount.current) { lastCount.current = count; scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); } }, [messages]); const canSendNip44 = !!user?.signer?.nip44; const handleSend = async (e: React.FormEvent) => { e.preventDefault(); const text = draft.trim(); if (!text || !user) return; if (!canSendNip44) { toast({ title: 'Encryption unavailable', description: 'Your signer does not support NIP-44 encryption. Please upgrade your extension.', variant: 'destructive', }); return; } // Recipients = everyone except yourself (the hook also wraps a copy to self). const recipients = participants.filter((p) => p !== user.pubkey); if (recipients.length === 0) { toast({ title: 'No one to message yet', description: 'This crew has no other members to chat with.', }); return; } try { await sendMessage({ recipients, content: text, subject: `${crewName} crew chat`, }); setDraft(''); } catch (err) { toast({ title: 'Message failed', description: err instanceof Error ? err.message : 'Could not send message.', variant: 'destructive', }); } }; return (
Crew Chat Private

Encrypted group chat for the founder & all listed crew members.

{!user ? (

Log in to see if you can join the crew chat.

) : !isMember ? (

This chat is private to crew members. Ask the founder to add you to the crew to join the conversation.

) : ( <>
{isLoading && Array.from({ length: 3 }).map((_, i) => (
))} {!isLoading && (!messages || messages.length === 0) && (

No messages yet. Kick off the crew chat — plan a session, share a spot, hype a clip.

)} {messages?.map((m) => ( ))}