import { useMemo } from 'react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useMyCrewInvites, useIncomingJoinRequests, useJoinRequestEndorsements } from '@/hooks/useCrewInvites'; import { useCrewDismissals } from '@/hooks/useCrewDismissals'; import { useCrews } from '@/hooks/useCrews'; import { useCollectiveMembers } from '@/hooks/useCollectiveMembers'; import { SKATE_CREW_KIND, type CrewEndorsement, type CrewInvite, type CrewJoinRequest, type SkateCrew } from '@/lib/types'; export interface CrewNotification { /** Stable id for the notification (invite id or crew coord). */ id: string; type: 'invite' | 'pending-confirm'; crew?: SkateCrew; invite?: CrewInvite; createdAt: number; } /** * The logged-in user's actionable crew notifications: * * - `invite`: a structured crew invite addressed to them (kind 36229). * - `pending-confirm`: a crew that has LISTED them as a member but which they * haven't confirmed yet (they were added directly, e.g. * via the founder editor before invites existed). * * Both resolve to the same "join this crew" action, so the notification center * can render them uniformly. */ export function useCrewNotifications() { const { user } = useCurrentUser(); const { data: invites, isLoading: invitesLoading } = useMyCrewInvites(user?.pubkey); const { data: joinRequests, isLoading: joinLoading } = useIncomingJoinRequests(user?.pubkey); const { data: crews, isLoading: crewsLoading } = useCrews(200); // Cross-device-synced set of dismissed invite/request ids. const { data: dismissed } = useCrewDismissals(); // Invites addressed to me, minus anything I've dismissed (accepted/declined), // synced across all my devices. const visibleInvites = useMemo(() => { const list = invites ?? []; if (!dismissed || dismissed.size === 0) return list; return list.filter((inv) => !dismissed.has(inv.id)); }, [invites, dismissed]); // Crews that list me as a member (candidates for "pending confirmation"). const listedCrews = useMemo(() => { if (!user || !crews) return []; return crews.filter( (c) => c.pubkey.toLowerCase() !== user.pubkey.toLowerCase() && c.members.some((m) => m.toLowerCase() === user.pubkey.toLowerCase()), ); }, [user, crews]); // Resolve each incoming join-request to its crew (for approval + display). const requestsWithCrew = useMemo(() => { if (!user || !joinRequests?.length || !crews) return []; return joinRequests // Drop dismissed requests (synced across the moderator's devices). .filter((req) => !dismissed?.has(req.id)) .map((req) => { const crew = crews.find( (c) => `${SKATE_CREW_KIND}:${c.pubkey}:${c.identifier}` === req.crewCoord, ); return crew ? { request: req, crew } : null; }) .filter((x): x is { request: CrewJoinRequest; crew: SkateCrew } => !!x) // Only show requests for crews I actually lead or help run. .filter( ({ crew }) => crew.pubkey.toLowerCase() === user.pubkey.toLowerCase() || crew.admins.some((a) => a.toLowerCase() === user.pubkey.toLowerCase()), ); }, [user, joinRequests, crews, dismissed]); // Batch-fetch admin endorsements for the visible requests (one query), keyed // per crew so only real admins of THAT crew can vouch. const requestIds = useMemo( () => requestsWithCrew.map(({ request }) => request.id), [requestsWithCrew], ); const adminsByCrewCoord = useMemo(() => { const map = new Map>(); for (const { request, crew } of requestsWithCrew) { map.set(request.crewCoord, new Set(crew.admins.map((a) => a.toLowerCase()))); } return map; }, [requestsWithCrew]); const { data: endorsements } = useJoinRequestEndorsements(requestIds, adminsByCrewCoord); // Attach each request's endorsements for the card to render. const requestsWithEndorsements = useMemo( () => requestsWithCrew.map((entry) => ({ ...entry, endorsements: (endorsements?.get(entry.request.id) ?? []) as CrewEndorsement[], })), [requestsWithCrew, endorsements], ); return { user, invites: visibleInvites, listedCrews, joinRequests: requestsWithEndorsements, isLoading: invitesLoading || joinLoading || crewsLoading, }; } /** * Resolve whether the logged-in user has CONFIRMED membership in a listed crew. * Used per-crew to decide if a "pending confirm" notification is still relevant. */ export function useIsPendingMember(crew: SkateCrew | undefined) { const { user } = useCurrentUser(); const { data: members } = useCollectiveMembers({ kind: SKATE_CREW_KIND, founderPubkey: crew?.pubkey ?? '', identifier: crew?.identifier ?? '', claimedMembers: crew?.members ?? [], enabled: !!crew && (crew.members.length ?? 0) > 0, }); if (!user || !crew) return false; const me = members?.find((m) => m.pubkey.toLowerCase() === user.pubkey.toLowerCase()); return me?.status === 'pending'; }