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) => (
))}
>
)}
);
}