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