feat: add message-list category tabs
This commit is contained in:
@@ -639,6 +639,119 @@ export interface EmailListBadge {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// ─── Message-list category tabs (Gmail-style inbox tabs) ─────
|
||||
//
|
||||
// The tab contract lives in core so tabs render natively: a plugin registers
|
||||
// tab DEFINITIONS via `api.tabs.set(config)`; the host renders the tab strip
|
||||
// above the message list, ANDs the active tab's filter into the mailbox
|
||||
// Email/query, and maintains per-tab unread badges.
|
||||
//
|
||||
// A tab selects its messages in one of two ways:
|
||||
// 1. `query` — a raw JMAP Email FilterCondition/FilterOperator evaluated
|
||||
// server-side at view time (search-based tabs). Nothing is written to
|
||||
// the messages; classification is a live query (e.g. match the
|
||||
// List-Unsubscribe header, or an OR of social sender domains).
|
||||
// 2. `keyword` — a `hasKeyword` filter (durable tabs). Classification then
|
||||
// happens out-of-band — typically a plugin-managed Sieve section at
|
||||
// delivery (see filterHooks.onSieveScriptGenerate) or explicit
|
||||
// recategorization via `api.tabs.categorize`.
|
||||
// A tab with NEITHER is the default bucket ("Primary"): it matches messages
|
||||
// that match none of the other tabs' filters (server-side NOT).
|
||||
|
||||
/**
|
||||
* One tab in the message-list tab strip.
|
||||
*/
|
||||
export interface MessageListTab {
|
||||
/** Stable id within the plugin, e.g. "promotions". Used in hooks and counts. */
|
||||
id: string;
|
||||
/** Display label (localize in the plugin via api.i18n before registering). */
|
||||
label: string;
|
||||
/**
|
||||
* Search-based selection: a JMAP Email FilterCondition or FilterOperator
|
||||
* (RFC 8621 §4.4.1), e.g. `{ header: ["List-Unsubscribe"] }` or
|
||||
* `{ operator: "OR", conditions: [{ from: "reddit.com" }, ...] }`.
|
||||
* The host ANDs it with the mailbox condition. Takes precedence over
|
||||
* `keyword` when both are set.
|
||||
*/
|
||||
query?: Record<string, unknown>;
|
||||
/**
|
||||
* Keyword-based selection, e.g. "$category-promotions" — filters via
|
||||
* `hasKeyword`. Use for durable, Sieve-assigned categories.
|
||||
*/
|
||||
keyword?: string | null;
|
||||
/** Lucide icon name rendered before the label (optional). */
|
||||
icon?: string;
|
||||
/** CSS color for the active-tab indicator / badge accent (optional). */
|
||||
color?: string;
|
||||
/** Sort order within the strip (ascending, default 100). */
|
||||
order?: number;
|
||||
/** Show the unread-count badge for this tab. Default true. */
|
||||
showUnreadBadge?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered by a plugin via `api.tabs.set(config)` (permission
|
||||
* `ui:message-list-tabs`). Cleared automatically when the plugin unloads,
|
||||
* or explicitly via `api.tabs.clear()`.
|
||||
*/
|
||||
export interface MessageListTabsConfig {
|
||||
tabs: MessageListTab[];
|
||||
/**
|
||||
* Mailbox roles the strip renders for. Default ['inbox'] — category tabs
|
||||
* on Trash or Sent are almost never what anyone wants.
|
||||
*/
|
||||
mailboxRoles?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed to messageListTabHooks.onTabActivate observers when the user
|
||||
* switches tabs.
|
||||
*/
|
||||
export interface TabActivateContext {
|
||||
tabId: string;
|
||||
previousTabId: string | null;
|
||||
/** Store id of the mailbox the strip is filtering. */
|
||||
mailboxId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed to messageListTabHooks.onBeforeEmailCategorize (intercept — return
|
||||
* false to cancel) and onEmailCategorize (observer, after the keyword patch
|
||||
* was applied). Describes a host-driven recategorization triggered by
|
||||
* `api.tabs.categorize(emailIds, toTabId)` or native UI. Only keyword-based
|
||||
* (or default) tabs can be categorize targets — search-based tabs derive
|
||||
* membership from their query, so a plugin implements "move to tab" there by
|
||||
* updating its query (e.g. a per-sender override) and re-registering.
|
||||
*/
|
||||
export interface EmailCategorizeContext {
|
||||
emailIds: string[];
|
||||
/** Target tab id. */
|
||||
toTabId: string;
|
||||
/** Keyword added by the move, or null when the target is the default tab. */
|
||||
keywordAdded: string | null;
|
||||
/** Category keywords removed from the messages by the move. */
|
||||
keywordsRemoved: string[];
|
||||
/**
|
||||
* Unique sender addresses of the affected messages — the hook payload a
|
||||
* plugin needs to offer Gmail-style "do this for all mail from X"
|
||||
* (per-sender overrides, Sieve regeneration).
|
||||
*/
|
||||
senders: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Second argument to filterHooks.onSieveScriptGenerate transform handlers.
|
||||
* The transform's value is the full Sieve script the host is about to upload
|
||||
* as the account's active script; handlers return a modified script (e.g.
|
||||
* with a plugin-managed categorizer section appended) or undefined to pass
|
||||
* through. Keep `require` statements at the top of the script — the host
|
||||
* uploads the returned text verbatim after validation.
|
||||
*/
|
||||
export interface SieveScriptGenerateContext {
|
||||
/** Account whose active script is being (re)generated. */
|
||||
accountId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed to onMailtoIntercept handlers.
|
||||
* Return false to prevent the browser from opening the system mail client.
|
||||
@@ -926,6 +1039,8 @@ export const ALL_PERMISSIONS = [
|
||||
'ui:observe', 'ui:toolbar', 'ui:app-top-banner', 'ui:email-banner', 'ui:email-footer',
|
||||
'ui:email-details',
|
||||
'ui:download-file',
|
||||
// Register native message-list category tabs (Gmail-style inbox tabs).
|
||||
'ui:message-list-tabs',
|
||||
'ui:composer-toolbar', 'ui:composer-sidebar',
|
||||
'ui:sidebar-widget', 'ui:settings-section',
|
||||
'ui:context-menu', 'ui:navigation-rail', 'ui:keyboard',
|
||||
@@ -941,6 +1056,9 @@ export const IMPLICIT_PERMISSIONS: Permission[] = ['ui:observe', 'app:lifecycle'
|
||||
|
||||
// ─── Validation ──────────────────────────────────────────────
|
||||
|
||||
/** Upper bound on tabs a single plugin may register via api.tabs.set. */
|
||||
export const MAX_MESSAGE_LIST_TABS = 8;
|
||||
|
||||
export const MAX_PLUGIN_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
export const MAX_THEME_SIZE = 2 * 1024 * 1024; // 2 MB (was 1 MB; v2 themes may ship a skin.css)
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user