/** * Contacts + inbound request tracking. * * - Loads cached contacts from local storage on boot. * - Subscribes to the address WS topic so a new CONTACT_REQUEST pulls the * relay contact list immediately (sub-second UX). * - Keeps a 30 s polling fallback for nodes without WS or while disconnected. */ import { useEffect, useCallback } from 'react'; import { fetchContactRequests } from '@/lib/api'; import { getWSClient } from '@/lib/ws'; import { loadContacts } from '@/lib/storage'; import { useStore } from '@/lib/store'; const FALLBACK_POLL_INTERVAL = 30_000; export function useContacts() { const keyFile = useStore(s => s.keyFile); const setContacts = useStore(s => s.setContacts); const setRequests = useStore(s => s.setRequests); const contacts = useStore(s => s.contacts); // Load cached contacts from local storage once useEffect(() => { loadContacts().then(setContacts); }, [setContacts]); const pollRequests = useCallback(async () => { if (!keyFile) return; try { const raw = await fetchContactRequests(keyFile.pub_key); // Filter out already-accepted contacts const contactAddresses = new Set(contacts.map(c => c.address)); const requests = raw .filter(r => r.status === 'pending' && !contactAddresses.has(r.requester_pub)) .map(r => ({ from: r.requester_pub, // x25519Pub will be fetched from identity when user taps Accept x25519Pub: '', intro: r.intro ?? '', timestamp: r.created_at, txHash: r.tx_id, })); setRequests(requests); } catch { // Ignore transient network errors } }, [keyFile, contacts, setRequests]); useEffect(() => { if (!keyFile) return; const ws = getWSClient(); // Initial load + low-frequency fallback poll (covers missed WS events, // works even when the node has no WS endpoint). pollRequests(); const interval = setInterval(pollRequests, FALLBACK_POLL_INTERVAL); // Immediate refresh when a CONTACT_REQUEST / ACCEPT_CONTACT tx addressed // to us lands on-chain. WS fan-out already filters to our address topic. const off = ws.subscribe('addr:' + keyFile.pub_key, (frame) => { if (frame.event === 'tx') { const d = frame.data as { tx_type?: string } | undefined; if (d?.tx_type === 'CONTACT_REQUEST' || d?.tx_type === 'ACCEPT_CONTACT') { pollRequests(); } } }); ws.connect(); return () => { clearInterval(interval); off(); }; }, [keyFile, pollRequests]); }