import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { useNostrLogin } from '@nostrify/react/login'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities'; import { buildEnvelope, fetchGroupMessages, publishEnvelope, type CordnRelay, } from '@/lib/cordn/client'; import { sealGroupMessage, type Nip44Signer } from '@/lib/cordn/crypto'; import type { CordnEndpoint, CordnMessage } from '@/lib/cordn/types'; import type { NostrEvent, NostrSigner } from '@nostrify/nostrify'; interface UseCordnChatParams { /** Resolved coordinator endpoint (relay + group id). `null` disables the chat. */ endpoint: CordnEndpoint | null; /** Full roster (founder + members) — recipients for group encryption. */ roster: string[]; /** Whether the viewer is a member (only members query/decrypt). */ enabled: boolean; } /** * Crew encrypted group chat over a Cordn coordinator. * * Fetch-then-poll: an initial bounded backlog fetch, then a light poll for live * delivery (5s) — robust in a browser PWA and coordinator-agnostic. Mirrors the * NIP-17 `useGroupConversation` / `useSendDirectMessage` API so `CrewChat` can * swap transports with no UI change. */ export function useCordnChat({ endpoint, roster, enabled }: UseCordnChatParams) { const { nostr } = useNostr(); const { user } = useCurrentUser(); const { logins } = useNostrLogin(); const queryClient = useQueryClient(); const canEncrypt = checkNip44(user, logins); const active = !!endpoint && !!user && enabled && canEncrypt; const query = useQuery({ queryKey: ['cordn-chat', endpoint?.relayUrl, endpoint?.groupId, user?.pubkey], enabled: active, refetchInterval: active ? 5000 : false, staleTime: 2000, queryFn: async (): Promise => { if (!endpoint || !user) return []; const relay = nostr.relay(endpoint.relayUrl) as unknown as CordnRelay; const signer = user.signer as unknown as Nip44Signer; const signal = AbortSignal.timeout(10000); return fetchGroupMessages({ relay, endpoint, signer, viewerPk: user.pubkey, signal, }); }, }); const send = useMutation({ mutationFn: async (text: string) => { const body = text.trim(); if (!body) return; if (!endpoint || !user) throw new Error('Crew chat is not available.'); if (!canEncrypt) throw new Error('Your signer does not support NIP-44 encryption.'); const signer = user.signer as unknown as Nip44Signer; const sealed = await sealGroupMessage(signer, user.pubkey, roster, body); const envelope: NostrEvent = await buildEnvelope( user.signer as unknown as NostrSigner, endpoint, JSON.stringify(sealed), ); const relay = nostr.relay(endpoint.relayUrl) as unknown as CordnRelay; await publishEnvelope(relay, envelope, AbortSignal.timeout(15000)); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cordn-chat', endpoint?.relayUrl, endpoint?.groupId, user?.pubkey], }); }, }); return { messages: query.data ?? [], isLoading: query.isLoading, isError: query.isError, canEncrypt, send: send.mutateAsync, isSending: send.isPending, }; }