diff --git a/src/components/CollectiveJoinRequestCard.tsx b/src/components/CollectiveJoinRequestCard.tsx index a156466..5869372 100644 --- a/src/components/CollectiveJoinRequestCard.tsx +++ b/src/components/CollectiveJoinRequestCard.tsx @@ -4,11 +4,13 @@ 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 { Users, X, Loader2, Hand, Settings2, Check } from 'lucide-react'; +import { UserPlus, X, Loader2, Hand, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; function npubSafe(pubkey: string): string { @@ -22,10 +24,11 @@ function npubSafe(pubkey: string): string { /** * An incoming "request to join this collective" for a Lab-collective owner. * - * Trust model mirrors crews: only the owner can edit their own Lab-profile - * event, so the card can't add the member for them. Instead it links straight - * to the collective's editor (where the roster lives) and lets the owner - * dismiss the request once handled. No forged membership is ever possible. + * 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, @@ -40,17 +43,46 @@ export function CollectiveJoinRequestCard({ const requesterName = requester.data?.metadata?.name ?? genUserName(request.requester); const requesterPic = requester.data?.metadata?.picture; const { toast } = useToast(); - const { mutateAsync: dismiss, isPending: dismissing } = useDismissCrewNotification(); - const [done, setDone] = useState(false); const coord = parseAddrCoord(request.collectiveCoord); - // All Lab crafts render + edit through the /coach detail route. 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(true); + setDone('dismissed'); onResolved?.(); } catch { toast({ title: 'Could not dismiss', variant: 'destructive' }); @@ -60,12 +92,24 @@ export function CollectiveJoinRequestCard({ if (done) { return (
- - Dismissed request from {requesterName} + {done === 'added' ? ( + <> + + Added {requesterName} to{' '} + {request.collectiveName} + + ) : ( + <> + + Dismissed request from {requesterName} + + )}
); } + const busy = approving || dismissing; + return (
)} -
- - -
- -

- - Add them from your collective’s member list, then dismiss this. -

+ {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. */ +
+ + +
+ ) : ( +
+ + +
+ )}
diff --git a/src/components/RequestToJoinCollective.tsx b/src/components/RequestToJoinCollective.tsx index 6040ae9..612a070 100644 --- a/src/components/RequestToJoinCollective.tsx +++ b/src/components/RequestToJoinCollective.tsx @@ -76,7 +76,7 @@ export function RequestToJoinCollective({ <>

Ask the crew behind {profile.title} to add - you to the roster. They confirm from their side. + you to the roster. They’ll get a notification and can add you in one tap.