use-whatsapp-sync-on-load.ts3.1 KBView on GitHub import { useCallback, useEffect, useRef } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { INBOX_REFRESH_EVENT } from '@/modules/inbox/hooks/use-inbox-items';
/** Don't re-pull Unipile more than once per this window (billable / rate-limited). */
const RESYNC_THROTTLE_MS = 60 * 1000;
/**
* Best-effort catch-up for WhatsApp — the channel analogue of email's on-load
* sync. Fires `outbound.whatsapp.syncChats`, which mirrors the rep's live Unipile
* chat list into `whatsapp_chats` (the DB mirror the feed reads), then calls
* `onSynced` to refetch. Runs on mount AND when the tab regains focus (throttled),
* so a feed left open doesn't stay stale/empty — the mirror is only refreshed by
* this sync. Fire-and-forget: errors are swallowed so a sync problem never blocks
* the inbox.
*
* NOTE: this triggers a Unipile pull (billable / rate-limited) and is not
* verifiable without a live WhatsApp seat. See omni-channel-inbox.md Phase 6.
*/
export function useWhatsappSyncOnLoad(opts: { enabled: boolean; onSynced: () => void }) {
const trpc = useTRPC();
const syncChats = useMutation(trpc.outbound.whatsapp.syncChats.mutationOptions());
const lastSyncedAt = useRef(0);
const inFlight = useRef(false);
const onSyncedRef = useRef(opts.onSynced);
onSyncedRef.current = opts.onSynced;
const runSync = useCallback(
(force = false) => {
if (!opts.enabled || inFlight.current) return;
if (!force && Date.now() - lastSyncedAt.current < RESYNC_THROTTLE_MS) return;
inFlight.current = true;
lastSyncedAt.current = Date.now();
void (async () => {
try {
await syncChats.mutateAsync({});
onSyncedRef.current();
} catch (err) {
// Don't block the inbox, but keep it diagnosable (live Unipile call).
// eslint-disable-next-line no-console
console.warn('[inbox] WhatsApp sync failed', err);
} finally {
inFlight.current = false;
}
})();
},
// syncChats mutation object is stable.
// eslint-disable-next-line react-hooks/exhaustive-deps
[opts.enabled],
);
// Initial sync on mount (force past the throttle).
useEffect(() => {
if (opts.enabled && lastSyncedAt.current === 0) runSync(true);
}, [opts.enabled, runSync]);
// Re-sync when the tab regains focus (throttled) or the user hits Refresh (forced),
// so an open feed surfaces new messages.
useEffect(() => {
if (!opts.enabled) return;
const onFocus = () => runSync();
const onVisible = () => {
if (document.visibilityState === 'visible') runSync();
};
const onRefresh = () => runSync(true);
window.addEventListener('focus', onFocus);
document.addEventListener('visibilitychange', onVisible);
window.addEventListener(INBOX_REFRESH_EVENT, onRefresh);
return () => {
window.removeEventListener('focus', onFocus);
document.removeEventListener('visibilitychange', onVisible);
window.removeEventListener(INBOX_REFRESH_EVENT, onRefresh);
};
}, [opts.enabled, runSync]);
}