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]);
}