import { useState } from 'react'; import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { useAuthor } from '@/hooks/useAuthor'; import { useLabProfile } from '@/hooks/useLabProfiles'; import { useApproveCollectiveJoinRequest } from '@/hooks/useCollectiveJoinRequests'; import { useDismissCrewNotification } from '@/hooks/useCrewDismissals'; import { useToast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; import { parseAddrCoord, type CollectiveJoinRequest } from '@/lib/types'; import { UserPlus, X, Loader2, Hand, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; function npubSafe(pubkey: string): string { try { return nip19.npubEncode(pubkey); } catch { return pubkey; } } /** * An incoming "request to join this collective" for a Lab-collective owner. * * One-tap flow: the owner sees who wants in and hits **Add to collective** — * that appends the requester to the owner's own Lab-profile roster and clears * the request. Only the owner can edit their own event, so no forged membership * is possible. While the profile loads (or if it can't be resolved) the card * degrades gracefully to a link + Dismiss so the owner is never stuck. */ export function CollectiveJoinRequestCard({ request, onResolved, compact, }: { request: CollectiveJoinRequest; onResolved?: () => void; compact?: boolean; }) { const requester = useAuthor(request.requester); const requesterName = requester.data?.metadata?.name ?? genUserName(request.requester); const requesterPic = requester.data?.metadata?.picture; const { toast } = useToast(); const coord = parseAddrCoord(request.collectiveCoord); const collectiveHref = coord ? `/coach/${coord.pubkey}/${coord.identifier}` : '/lab'; // Resolve the collective so we can add the member in one click. const { data: profile, isLoading: profileLoading } = useLabProfile( coord?.identifier ?? '', coord?.pubkey, ); const { mutateAsync: approve, isPending: approving } = useApproveCollectiveJoinRequest(); const { mutateAsync: dismiss, isPending: dismissing } = useDismissCrewNotification(); const [done, setDone] = useState<'added' | 'dismissed' | null>(null); const alreadyMember = !!profile && profile.members.some((m) => m.toLowerCase() === request.requester.toLowerCase()); const handleAdd = async () => { if (!profile) return; try { await approve({ request, profile }); setDone('added'); toast({ title: `Added ${requesterName} to ${request.collectiveName} 🛹`, description: "They'll confirm from their side to appear as a verified member.", }); onResolved?.(); } catch (err) { toast({ title: 'Could not add member', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive', }); } }; const handleDismiss = async () => { try { await dismiss(request.id); setDone('dismissed'); onResolved?.(); } catch { toast({ title: 'Could not dismiss', variant: 'destructive' }); } }; if (done) { return (
{done === 'added' ? ( <> Added {requesterName} to{' '} {request.collectiveName} ) : ( <> Dismissed request from {requesterName} )}
); } const busy = approving || dismissing; return (

{requesterName.slice(0, 2).toUpperCase()} {requesterName} {' '} wants to join{' '} {request.collectiveName}

{request.note && (

“{request.note}”

)} {alreadyMember ? (

Already on the roster

) : !profileLoading && !profile ? ( /* Profile couldn't be resolved — never trap the owner. Offer a link to open the collective plus a way to clear the notification. */
) : (
)}
); }