inbox-query-params.ts3.4 KBView on GitHub
import { normalizeSplitExclusionQuery } from '@/modules/threads/lib/split-query-normalization';

type InboxExclusionEntry = {
  // System inboxes have no `query`, so the filter inside
  // `buildImportantQueryExclusions` skips them — pass the full inbox list
  // (system + custom) directly.
  id: string;
  enabled?: boolean;
  alsoShowInImportant?: boolean;
  query?: string;
  // Presence of a CRM rule disqualifies the inbox from exclusion math entirely.
  // Only the flag matters here, never its contents.
  conversationFilter?: unknown;
};

export type ImportantSignal = 'category_personal' | 'gmail_important';

type ImportantIncludeQueryInput = {
  hasCategoryPersonal: boolean;
  connectionEmail?: string | null;
  sessionName?: string | null;
  /**
   * Which Gmail signal counts as "Important". Default is 'category_personal'
   * (the original behavior). 'gmail_important' substitutes Gmail's
   * `label:IMPORTANT` instead of `category:personal` and ignores
   * `hasCategoryPersonal` (the user-level category enablement check).
   */
  signal?: ImportantSignal;
};

export function buildImportantIncludeTerms(input: ImportantIncludeQueryInput): string[] {
  const includeTerms: string[] = [];

  if (input.signal === 'gmail_important') {
    includeTerms.push('label:IMPORTANT');
  } else if (input.hasCategoryPersonal) {
    includeTerms.push('category:personal');
  }

  const senderTerms: string[] = [];
  if (input.connectionEmail?.trim()) {
    senderTerms.push(input.connectionEmail.trim());
  }
  if (input.sessionName?.trim()) {
    senderTerms.push(`"${input.sessionName.trim()}"`);
  }
  if (senderTerms.length > 0) {
    includeTerms.push(`from:(${senderTerms.join(' OR ')})`);
  }

  includeTerms.push('from:"via Google"');
  return includeTerms;
}

export function joinAsOrGroup(terms: string[]): string {
  if (terms.length === 0) return '';
  if (terms.length === 1) return `(${terms[0]})`;
  return `((${terms.join(') OR (')}))`;
}

export function buildImportantIncludeQuery(input: ImportantIncludeQueryInput): string {
  const includeClause = joinAsOrGroup(buildImportantIncludeTerms(input));
  return includeClause ? `label:INBOX ${includeClause}` : 'label:INBOX';
}

/**
 * The queries to subtract from the default / Important inbox, so a custom inbox's
 * threads appear in one place rather than two.
 *
 * A CRM-filtered inbox is UNCONDITIONALLY excluded from this list — even when its
 * `alsoShowInImportant` is false. Its real rule lives in `conversationFilter`; its
 * `query` is only the mailbox-scope anchor, which for those inboxes is a bare
 * `label:INBOX`. Subtracting that would compile the default inbox to
 * `label:INBOX -(label:INBOX)` and render the user's entire Inbox empty. The
 * `alsoShowInImportant` default of `true` is the first guard; this is the one that
 * survives a user un-ticking the toggle.
 */
export function buildImportantQueryExclusions(inboxes: InboxExclusionEntry[]): string[] {
  return [
    ...new Set(
      inboxes
        .filter(
          (inbox): inbox is InboxExclusionEntry & { query: string } =>
            inbox.id !== 'important' &&
            inbox.enabled !== false &&
            !inbox.alsoShowInImportant &&
            !inbox.conversationFilter &&
            typeof inbox.query === 'string' &&
            inbox.query.trim().length > 0,
        )
        .map((inbox) => normalizeSplitExclusionQuery(inbox.query)),
    ),
  ];
}