route-inbox.ts5.1 KBView on GitHub
import {
  DEFAULT_INBOX,
  findInboxBySlug,
  slugifyInboxName,
  type InboxConfig,
} from '@/modules/threads/hooks/use-inboxes';
import {
  getSystemFolderSplit,
  type SystemFolderSplit,
} from '@/modules/threads/lib/system-folder-splits';

/**
 * Which inbox a `/mail/:folder` route is on — resolved from the URL, for every surface.
 *
 * The email list and the unified feed used to answer this differently: `useThreads` fell back
 * to the PERSISTED `settings.activeInboxId` while the feed read the slug, so on `/mail/inbox`
 * with any other tab active the two asked different questions before the server was reached —
 * one list showing Important under a tab claiming to be the whole Inbox. The URL wins: it is
 * what the user navigated to, it is what is shareable, and `activeInboxId` is a preference
 * that trails it (the folder route syncs the setting FROM the slug, never the reverse).
 */
export type RouteInboxResolution = {
  /**
   * The inbox this route means, or `undefined` when the route is not an inbox route at all
   * (a Gmail label folder, a search). Callers gate on their own "is this a compiled-query
   * route" rule before using it.
   */
  inbox: InboxConfig | undefined;
  /**
   * True when the slug itself resolved — a stored inbox, an id fallback, or a system folder
   * split. False for the `/mail/inbox` default and for non-inbox routes, which is what tells
   * a caller whether the route's own query or a folder mapping is in play.
   */
  matchedSlug: boolean;
  /** Set when the slug names a hardcoded folder template (sent / archive / …), not an inbox. */
  systemFolderSplit: SystemFolderSplit | undefined;
  /**
   * The persisted (or system-stub) record this route maps to. Only a record carries a CRM
   * rule, so this — not `inbox` — is what may be sent as `inboxId`. A system folder split
   * resolves to none.
   */
  record: InboxConfig | undefined;
};

/**
 * A custom inbox reached by its raw id, or by a slug that does not normalize to its name.
 * `inbox` is excluded because it is the default route rather than a name to match.
 */
function findInboxByFolderSlug(folder: string, inboxes: InboxConfig[]): InboxConfig | undefined {
  const normalizedFolder = folder.trim().toLowerCase();
  if (!normalizedFolder || normalizedFolder === 'inbox') return undefined;

  return inboxes.find(
    (inbox) =>
      !inbox.system &&
      (slugifyInboxName(inbox.name) === normalizedFolder ||
        inbox.id.toLowerCase() === normalizedFolder),
  );
}

/** The inbox record backing a resolved inbox, by id and then by name for legacy stragglers. */
function findRecord(inbox: InboxConfig | undefined, inboxes: InboxConfig[]): InboxConfig | undefined {
  if (!inbox) return undefined;
  return inboxes.find(
    (candidate) =>
      candidate.id === inbox.id || candidate.name.toLowerCase() === inbox.name.toLowerCase(),
  );
}

export function resolveRouteInbox(params: {
  inboxes: InboxConfig[];
  folder: string;
  /**
   * A caller that renders several inboxes under one slug (the stacked sections) names its
   * own. It short-circuits the ladder rather than joining it — the URL is not describing it.
   */
  explicitInbox?: InboxConfig;
}): RouteInboxResolution {
  const { inboxes, folder, explicitInbox } = params;

  if (explicitInbox) {
    return {
      inbox: explicitInbox,
      matchedSlug: true,
      systemFolderSplit: undefined,
      record: findRecord(explicitInbox, inboxes),
    };
  }

  const fromSlug = findInboxBySlug(inboxes, folder);
  if (fromSlug) {
    return {
      inbox: fromSlug,
      matchedSlug: true,
      systemFolderSplit: undefined,
      record: findRecord(fromSlug, inboxes),
    };
  }

  const byId = findInboxByFolderSlug(folder, inboxes);
  if (byId) {
    return {
      inbox: byId,
      matchedSlug: true,
      systemFolderSplit: undefined,
      record: findRecord(byId, inboxes),
    };
  }

  const systemFolderSplit = getSystemFolderSplit(folder);
  if (systemFolderSplit) {
    return {
      // A folder template is not an inbox record — it is synthesised so every caller has one
      // shape to read, and it deliberately resolves to no `record`: it carries no CRM rule.
      inbox: {
        id: systemFolderSplit.key,
        name: systemFolderSplit.inboxName,
        position: 0,
        system: true,
        rule: { kind: 'all' },
        query: systemFolderSplit.compiledQuery,
      },
      matchedSlug: true,
      systemFolderSplit,
      record: undefined,
    };
  }

  // `/mail/inbox` under a layout where no inbox owns that slug — the important/other split,
  // where the folder route is already navigating to the first tab. The whole inbox is what
  // the URL says while that redirect lands; the persisted active tab is a different question,
  // and answering it here is what made the two lists disagree.
  if (folder === 'inbox') {
    const wholeInbox = inboxes.find((inbox) => inbox.id === DEFAULT_INBOX.id) ?? DEFAULT_INBOX;
    return {
      inbox: wholeInbox,
      matchedSlug: false,
      systemFolderSplit: undefined,
      record: findRecord(wholeInbox, inboxes),
    };
  }

  return { inbox: undefined, matchedSlug: false, systemFolderSplit: undefined, record: undefined };
}