import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCrewChatRead } from '@/hooks/useCrewChatRead'; import { resolveCordnEndpoint } from '@/lib/cordn/endpoint'; import { CORDN_APP_TAG, CORDN_MESSAGE_KIND } from '@/lib/cordn/types'; import { SKATE_CREW_KIND, type SkateCrew } from '@/lib/types'; /** * A cheap, honest "is there new crew chat since I last looked?" signal for the * Chat tab's unread dot. * * Design constraints (kept idiot-proof and light): * - It must NOT decrypt every message in the background just to power a dot. * - It must NOT fire false pings. * * So it only reports unread for **Cordn** crews, where the newest message time * is a pure metadata query (`#a` group + newest `created_at`) — no decryption. * For the built-in NIP-17 transport, gift wraps use randomized timestamps and * require per-message decryption, so a reliable cheap peek isn't possible; there * we return `false` (the in-chat "New" divider already covers "what did I miss" * once the tab is opened). Only members query at all. * * Runs only while a crew page is mounted, for a single crew — never fanned out * across all crews. */ export function useCrewChatUnread(crew: SkateCrew | undefined, isMember: boolean) { const { nostr } = useNostr(); const { user } = useCurrentUser(); const crewCoord = crew ? `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}` : undefined; const { lastSeen } = useCrewChatRead(crewCoord); const endpoint = useMemo( () => (crew ? resolveCordnEndpoint(crew) : null), [crew], ); // Only meaningful for Cordn crews the viewer belongs to. const enabled = !!user && isMember && !!endpoint; const { data: newestAt = 0 } = useQuery({ queryKey: ['crew-chat-unread', endpoint?.relayUrl, endpoint?.groupId, user?.pubkey], enabled, staleTime: 15_000, refetchInterval: enabled ? 30_000 : false, queryFn: async (): Promise => { if (!endpoint) return 0; const relay = nostr.relay(endpoint.relayUrl); const signal = AbortSignal.timeout(8000); // Metadata-only: newest envelope for this group. No decryption. const events = await relay.query( [{ kinds: [CORDN_MESSAGE_KIND], '#a': [endpoint.groupId], '#t': [CORDN_APP_TAG], limit: 1, }], { signal }, ); return events.reduce((max, e) => Math.max(max, e.created_at), 0); }, }); // Unread only when there's a genuinely newer message than the last visit. const hasUnread = enabled && newestAt > (lastSeen ?? 0); return { hasUnread }; }