mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
393 lines
19 KiB
TypeScript
393 lines
19 KiB
TypeScript
// Settings search index helpers: which translation subtrees each settings tab
|
|
// renders, extra keywords per tab, and the walker that turns those subtrees
|
|
// into clickable sub-results. Kept free of React so it can be unit-tested.
|
|
|
|
export type SettingsSearchTab =
|
|
| 'account'
|
|
| 'general'
|
|
| 'notifications'
|
|
| 'appearance'
|
|
| 'message_list'
|
|
| 'reading'
|
|
| 'composing'
|
|
| 'vacation'
|
|
| 'filters'
|
|
| 'organize'
|
|
| 'security'
|
|
| 'sharing'
|
|
| 'recovery'
|
|
| 'calendar'
|
|
| 'contacts'
|
|
| 'files'
|
|
| 'addons'
|
|
| 'backup';
|
|
|
|
type Tab = SettingsSearchTab;
|
|
|
|
/** A row a settings page draws only in some situations. */
|
|
export type SettingsSearchCondition =
|
|
/** A touch screen (pointer: coarse), e.g. the swipe actions. */
|
|
| 'touch'
|
|
/** The Mountain View interface layout. */
|
|
| 'mountainView'
|
|
/** More than one account signed in. */
|
|
| 'multiAccount';
|
|
|
|
export interface SettingsSearchEntry {
|
|
/**
|
|
* Translation key of the label the page draws for the setting. The page
|
|
* marks that row with the same text in `data-search-label`, so a result
|
|
* always lands on its row (a test renders every page and checks).
|
|
*/
|
|
label: string;
|
|
/** The row is hidden until the Advanced switch is on. */
|
|
advanced?: true;
|
|
/** The page draws the row only then. */
|
|
when?: SettingsSearchCondition;
|
|
}
|
|
|
|
// The settings each page draws, as the search lists them. Only labels a page
|
|
// renders belong here: settings-search-anchors.test.tsx renders every page
|
|
// and fails for an entry without its row. A description is found next to
|
|
// the label (see descriptionKeyFor) and searched too.
|
|
export const tabSearchEntries: Record<Tab, SettingsSearchEntry[]> = {
|
|
account: [
|
|
{ label: 'settings.account.name_label' },
|
|
{ label: 'settings.account.email.label' },
|
|
{ label: 'settings.account.auth_method_label' },
|
|
{ label: 'settings.account.server.label', advanced: true },
|
|
{ label: 'settings.account.sections.addresses' },
|
|
{ label: 'settings.identities.identities_count.label' },
|
|
{ label: 'settings.identities.sub_addressing.label' },
|
|
{ label: 'settings.email_behavior.sub_address_delimiter.label', advanced: true },
|
|
{ label: 'settings.account.accounts.title', when: 'multiAccount' },
|
|
],
|
|
general: [
|
|
{ label: 'settings.language_region.title' },
|
|
{ label: 'settings.language_region.language.label' },
|
|
{ label: 'settings.language_region.date_format.label' },
|
|
{ label: 'settings.language_region.date_locale.label', advanced: true },
|
|
{ label: 'settings.language_region.time_format.label' },
|
|
{ label: 'settings.language_region.time_zone.label' },
|
|
{ label: 'settings.language_region.first_day.label' },
|
|
{ label: 'protocol_handlers.title' },
|
|
{ label: 'protocol_handlers.mailto_label' },
|
|
{ label: 'protocol_handlers.webcal_label' },
|
|
{ label: 'protocol_handlers.protocol_open_mode_label', advanced: true },
|
|
{ label: 'tour.settings_section' },
|
|
{ label: 'tour.restart_title' },
|
|
{ label: 'tour.show_on_new_devices_title', advanced: true },
|
|
],
|
|
notifications: [
|
|
{ label: 'settings.notifications.email.title' },
|
|
{ label: 'settings.notifications.email.enabled' },
|
|
{ label: 'settings.notifications.email.sound' },
|
|
{ label: 'settings.appearance.favicon_unread_badge.label' },
|
|
{ label: 'settings.notifications.calendar.title' },
|
|
{ label: 'settings.notifications.calendar.enabled' },
|
|
{ label: 'settings.notifications.sound_selection.title' },
|
|
{ label: 'settings.notifications.sound_selection.choose' },
|
|
{ label: 'settings.notifications.push.title' },
|
|
{ label: 'settings.notifications.push.relay_label', advanced: true },
|
|
{ label: 'settings.notifications.push.devices_title', advanced: true },
|
|
],
|
|
appearance: [
|
|
{ label: 'settings.appearance.theme.label' },
|
|
{ label: 'settings.themes.title' },
|
|
{ label: 'settings.appearance.sections.text' },
|
|
{ label: 'settings.appearance.font_size.label' },
|
|
{ label: 'settings.appearance.list_density.label' },
|
|
{ label: 'settings.appearance.animations.label' },
|
|
{ label: 'settings.appearance.sections.layout' },
|
|
{ label: 'settings.appearance.interface_layout.label' },
|
|
{ label: 'settings.email_behavior.mail_layout.label' },
|
|
{ label: 'settings.appearance.toolbar_position.label' },
|
|
{ label: 'settings.appearance.toolbar_labels.label' },
|
|
{ label: 'settings.appearance.sections.sidebar' },
|
|
{ label: 'settings.appearance.colorful_sidebar_icons.label' },
|
|
{ label: 'settings.appearance.show_folder_total_count.label' },
|
|
{ label: 'settings.appearance.sections.experimental', advanced: true },
|
|
{ label: 'settings.appearance.pro_interface.label', advanced: true },
|
|
{ label: 'settings.appearance.hide_account_switcher.label', when: 'multiAccount' },
|
|
{ label: 'settings.appearance.show_rail_account_list.label', when: 'multiAccount' },
|
|
],
|
|
message_list: [
|
|
{ label: 'settings.email_behavior.group_conversations.label' },
|
|
{ label: 'settings.email_behavior.show_preview.label' },
|
|
{ label: 'settings.appearance.message_list_order.label' },
|
|
{ label: 'settings.appearance.message_list_order.scope.label' },
|
|
{ label: 'settings.appearance.tint_list_rows.label' },
|
|
{ label: 'settings.email_behavior.show_verification_codes.label' },
|
|
{ label: 'settings.message_list.sections.senders' },
|
|
{ label: 'settings.advanced.sender_favicons.label' },
|
|
{ label: 'settings.advanced.sender_favicons.refresh_label' },
|
|
{ label: 'settings.advanced.show_avatars_in_junk.label', advanced: true },
|
|
{ label: 'settings.message_list.sections.actions' },
|
|
{ label: 'settings.email_behavior.hover_actions.label' },
|
|
{ label: 'settings.message_list.sections.delete' },
|
|
{ label: 'settings.email_behavior.delete_action.label' },
|
|
{ label: 'settings.email_behavior.archive_mode.label' },
|
|
{ label: 'settings.email_behavior.confirm_bulk_delete.label' },
|
|
{ label: 'settings.email_behavior.permanently_delete_junk.label', advanced: true },
|
|
{ label: 'settings.message_list.sections.loading', advanced: true },
|
|
{ label: 'settings.email_behavior.emails_per_page.label', advanced: true },
|
|
{ label: 'settings.email_behavior.clear_search_on_folder_change.label', advanced: true },
|
|
{ label: 'settings.email_behavior.swipe_right_action.label', when: 'touch' },
|
|
{ label: 'settings.email_behavior.swipe_left_action.label', when: 'touch' },
|
|
{ label: 'settings.email_behavior.inbox_type.label', when: 'mountainView' },
|
|
],
|
|
reading: [
|
|
{ label: 'settings.email_behavior.mark_read.label' },
|
|
{ label: 'settings.email_behavior.return_to_list_after_action.label' },
|
|
{ label: 'settings.reading_page.sections.display' },
|
|
{ label: 'settings.email_behavior.message_spacing.label' },
|
|
{ label: 'settings.email_behavior.plain_text_font.label' },
|
|
{ label: 'settings.email_behavior.always_light_mode.label' },
|
|
{ label: 'settings.reading_page.sections.images' },
|
|
{ label: 'settings.email_behavior.external_content.label' },
|
|
{ label: 'settings.email_behavior.trusted_senders.label' },
|
|
{ label: 'settings.email_behavior.trusted_senders.use_address_book_label' },
|
|
{ label: 'settings.reading_page.sections.attachments' },
|
|
{ label: 'settings.email_behavior.attachment_position.label' },
|
|
{ label: 'settings.email_behavior.attachment_click_action.label' },
|
|
{ label: 'settings.email_behavior.attachment_image_previews.label' },
|
|
{ label: 'settings.email_behavior.hide_inline_image_attachments.label' },
|
|
{ label: 'settings.downloads.title', advanced: true },
|
|
{ label: 'settings.downloads.email_template.label', advanced: true },
|
|
{ label: 'settings.downloads.attachment_template.label', advanced: true },
|
|
{ label: 'settings.downloads.bundle_template.label', advanced: true },
|
|
{ label: 'settings.downloads.spaces.label', advanced: true },
|
|
{ label: 'settings.downloads.lowercase.label', advanced: true },
|
|
{ label: 'settings.downloads.strip_diacritics.label', advanced: true },
|
|
{ label: 'settings.downloads.collapse_separators.label', advanced: true },
|
|
{ label: 'settings.downloads.after_export.label', advanced: true },
|
|
],
|
|
composing: [
|
|
{ label: 'settings.email_behavior.plain_text_mode.label' },
|
|
{ label: 'settings.email_behavior.recipient_mentions.label' },
|
|
{ label: 'settings.email_behavior.mention_adds_recipient.label' },
|
|
{ label: 'settings.email_behavior.rtl_editing.label', advanced: true },
|
|
{ label: 'settings.composing_page.sections.replying' },
|
|
{ label: 'settings.email_behavior.quick_reply.label' },
|
|
{ label: 'settings.email_behavior.auto_select_reply_identity.label' },
|
|
{ label: 'settings.email_behavior.signature_position.label' },
|
|
{ label: 'settings.email_behavior.signature_separator.label', advanced: true },
|
|
{ label: 'settings.composing_page.sections.sending' },
|
|
{ label: 'settings.email_behavior.send_delay.label' },
|
|
{ label: 'settings.email_behavior.attachment_reminder.label' },
|
|
{ label: 'settings.email_behavior.empty_subject_warning.label' },
|
|
{ label: 'settings.email_behavior.placeholder_warning.label' },
|
|
{ label: 'settings.composing_page.sections.receipts' },
|
|
{ label: 'settings.email_behavior.request_read_receipt.label' },
|
|
{ label: 'settings.email_behavior.read_receipt_response.label' },
|
|
{ label: 'settings.templates.title' },
|
|
{ label: 'settings.templates.export_import', advanced: true },
|
|
{ label: 'settings.email_behavior.signatures_link.label' },
|
|
],
|
|
vacation: [
|
|
{ label: 'settings.vacation.date_range.start' },
|
|
{ label: 'settings.vacation.date_range.end' },
|
|
{ label: 'settings.vacation.auto_reply.title' },
|
|
{ label: 'settings.vacation.status.label' },
|
|
{ label: 'settings.vacation.message.subject_label' },
|
|
{ label: 'settings.vacation.message.html_label' },
|
|
{ label: 'settings.vacation.forward.title' },
|
|
{ label: 'settings.vacation.forward.enabled_label' },
|
|
],
|
|
filters: [
|
|
{ label: 'settings.filters.title' },
|
|
{ label: 'settings.filters.add_rule' },
|
|
{ label: 'settings.filters.raw_editor' },
|
|
],
|
|
organize: [
|
|
{ label: 'settings.folders.folder_list' },
|
|
{ label: 'settings.folders.standard_roles', advanced: true },
|
|
{ label: 'settings.folders.role_inbox', advanced: true },
|
|
{ label: 'settings.folders.role_drafts', advanced: true },
|
|
{ label: 'settings.folders.role_sent', advanced: true },
|
|
{ label: 'settings.folders.role_trash', advanced: true },
|
|
{ label: 'settings.folders.role_junk', advanced: true },
|
|
{ label: 'settings.folders.role_archive', advanced: true },
|
|
{ label: 'settings.keywords.title' },
|
|
{ label: 'settings.keywords.nesting.label' },
|
|
],
|
|
security: [
|
|
{ label: 'settings.security.title' },
|
|
{ label: 'settings.security.totp.label' },
|
|
{ label: 'settings.security.sections.devices' },
|
|
{ label: 'settings.security.sections.encryption', advanced: true },
|
|
{ label: 'settings.security.password.title' },
|
|
{ label: 'settings.security.totp.section_title' },
|
|
{ label: 'settings.security.app_passwords.title' },
|
|
{ label: 'settings.security.api_keys.title', advanced: true },
|
|
{ label: 'settings.security.encryption.section_title', advanced: true },
|
|
{ label: 'settings.security.email_client.title' },
|
|
{ label: 'settings.security.link_device.title' },
|
|
],
|
|
sharing: [
|
|
{ label: 'delegates.title' },
|
|
],
|
|
recovery: [
|
|
{ label: 'recover_deleted.title' },
|
|
],
|
|
calendar: [
|
|
{ label: 'calendar.settings.title' },
|
|
{ label: 'calendar.settings.default_view' },
|
|
{ label: 'calendar.settings.invitation_parsing' },
|
|
{ label: 'calendar.settings.show_time_in_month_view' },
|
|
{ label: 'calendar.settings.show_week_numbers' },
|
|
{ label: 'calendar.settings.limit_hours' },
|
|
{ label: 'calendar.settings.visible_hours' },
|
|
{ label: 'calendar.settings.hide_non_working_days' },
|
|
{ label: 'calendar.settings.calendar_free_scroll' },
|
|
{ label: 'calendar.settings.hover_preview' },
|
|
{ label: 'calendar.settings.show_birthday_calendar' },
|
|
{ label: 'calendar.settings.enable_tasks' },
|
|
{ label: 'calendar.management.title' },
|
|
{ label: 'calendar.import.title' },
|
|
{ label: 'calendar.subscription.title' },
|
|
],
|
|
contacts: [
|
|
{ label: 'settings.contacts.group_by_letter_label' },
|
|
{ label: 'settings.contacts.sort_by_last_name_label' },
|
|
{ label: 'settings.contacts.import_label' },
|
|
{ label: 'settings.contacts.export_label' },
|
|
{ label: 'settings.contacts.manage_title' },
|
|
{ label: 'settings.contacts.categories_title' },
|
|
],
|
|
files: [
|
|
{ label: 'settings.files.display.title' },
|
|
{ label: 'settings.files.default_view.label' },
|
|
{ label: 'settings.files.default_sort.label' },
|
|
{ label: 'settings.files.sort_direction.label' },
|
|
{ label: 'settings.files.icons.title' },
|
|
{ label: 'settings.files.show_icons.label' },
|
|
{ label: 'settings.files.colored_icons.label' },
|
|
{ label: 'settings.files.show_thumbnails.label' },
|
|
{ label: 'settings.files.behavior.title', advanced: true },
|
|
{ label: 'settings.files.show_hidden.label', advanced: true },
|
|
],
|
|
addons: [
|
|
{ label: 'settings.sidebar_apps.title' },
|
|
{ label: 'settings.sidebar_apps.keep_loaded', advanced: true },
|
|
],
|
|
backup: [
|
|
{ label: 'settings.advanced.sections.backup' },
|
|
{ label: 'settings.advanced.export_settings.label' },
|
|
{ label: 'settings.advanced.import_settings.label' },
|
|
{ label: 'settings.advanced.sections.fix' },
|
|
{ label: 'settings.advanced.refresh_cache.label' },
|
|
{ label: 'settings.advanced.reset_settings.label' },
|
|
{ label: 'settings.advanced.debug_mode.label', advanced: true },
|
|
],
|
|
};
|
|
|
|
// Extra English keywords per tab so common search terms hit even when the
|
|
// translation doesn't contain the literal word.
|
|
/** Settings shown only while the admin has the Archive action on (#1257). */
|
|
export const archiveActionSearchPaths: ReadonlySet<string> = new Set(['settings.email_behavior.archive_mode.label']);
|
|
|
|
export const tabKeywords: Record<Tab, string> = {
|
|
account: 'profile email user signin signout reorder rearrange drag dropdown switcher multi-account identities from address signature plus addressing unified mailbox shared group',
|
|
general: 'language locale region timezone date time format mailto webcal links default app protocol handler tour onboarding',
|
|
notifications: 'sound alert push badge favicon tab icon unread count background relay device',
|
|
appearance: 'theme dark light font size accent color animation density custom theme css skin toolbar sidebar account switcher icons rail layout reading pane',
|
|
message_list: 'inbox list preview thread conversation grouping sort order columns archive delete swipe hover favicon avatar tag color',
|
|
reading: 'mark read attachment open monospace mono font plain text images remote content block sender privacy tracking dark light download filename template eml save export',
|
|
composing: 'editor signature plain text reply forward draft compose template snippet quick reply mention undo send receipt attachment reminder',
|
|
vacation: 'auto reply away out of office holiday responder',
|
|
filters: 'sieve rules add create new block junk forward',
|
|
organize: 'folders mailbox subscribe roles tags labels keywords colors nested',
|
|
security: 'password 2fa two-factor passkey app password mfa api key token phone mobile app pair qr thunderbird outlook imap client encryption pgp smime',
|
|
sharing: 'delegate delegation share access assistant permissions revoke folders calendars contacts colleague',
|
|
recovery: 'recover restore undelete deleted trash emptied lost messages',
|
|
calendar: 'event schedule appointment meeting timezone invitation ics tasks birthday working hours',
|
|
contacts: 'address book contact',
|
|
files: 'attachments cloud drive storage upload',
|
|
addons: 'apps webview iframe plugins extensions addons sidebar',
|
|
backup: 'export import storage quota privacy backup reset sync cache about version debug logs developer console diagnostic',
|
|
};
|
|
|
|
// Some settings texts name the app with {appName} (#1314). The index keeps
|
|
// them filled in, so the search matches the name people see and a result
|
|
// never carries the raw placeholder.
|
|
export function withAppName(node: unknown, appName: string): unknown {
|
|
if (typeof node === 'string') return node.split('{appName}').join(appName);
|
|
if (Array.isArray(node)) return node.map((item) => withAppName(item, appName));
|
|
if (node && typeof node === 'object') {
|
|
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, withAppName(value, appName)]));
|
|
}
|
|
return node;
|
|
}
|
|
|
|
export interface SubResult {
|
|
label: string;
|
|
description?: string;
|
|
// For plugin setting fields: the id of the plugin whose card needs to be
|
|
// expanded before the field becomes visible in the DOM.
|
|
pluginId?: string;
|
|
/** The row is hidden until the Advanced switch is on. */
|
|
advanced?: boolean;
|
|
}
|
|
|
|
/**
|
|
* The key of the text a page shows under a label: `x.label` and `x.title`
|
|
* have `x.description`, `x_label` has `x_description`, and a flat `x` (the
|
|
* calendar settings) has `x_desc`.
|
|
*/
|
|
export function descriptionKeyFor(label: string, messages: unknown): string | undefined {
|
|
const candidates = [
|
|
label.replace(/\.(label|title)$/, '.description'),
|
|
label.replace(/_(label|title)$/, '_description'),
|
|
`${label}_desc`,
|
|
`${label}_description`,
|
|
].filter((key) => key !== label);
|
|
return candidates.find((key) => typeof getByPath(messages, key) === 'string');
|
|
}
|
|
|
|
/**
|
|
* The search results a page offers, in the language of `messages`:
|
|
* each entry's label and description, with the app's name filled in.
|
|
* `shown` leaves out entries the page doesn't draw right now.
|
|
*/
|
|
export function settingsSubResults(
|
|
messages: unknown,
|
|
tab: SettingsSearchTab,
|
|
appName: string,
|
|
shown: (entry: SettingsSearchEntry) => boolean = () => true,
|
|
): SubResult[] {
|
|
const list: SubResult[] = [];
|
|
for (const entry of tabSearchEntries[tab]) {
|
|
if (!shown(entry)) continue;
|
|
const label = getByPath(messages, entry.label);
|
|
if (typeof label !== 'string') continue;
|
|
const descriptionKey = descriptionKeyFor(entry.label, messages);
|
|
const description = descriptionKey ? (getByPath(messages, descriptionKey) as string) : undefined;
|
|
list.push({
|
|
label: withAppName(label, appName) as string,
|
|
...(description ? { description: withAppName(description, appName) as string } : {}),
|
|
...(entry.advanced ? { advanced: true } : {}),
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/** Every word of the query is somewhere in the text, in any order. */
|
|
export function matchesAllWords(text: string, query: string): boolean {
|
|
const haystack = text.toLowerCase();
|
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
return words.length > 0 && words.every((word) => haystack.includes(word));
|
|
}
|
|
|
|
export function getByPath(obj: unknown, path: string): unknown {
|
|
let cur: unknown = obj;
|
|
for (const key of path.split('.')) {
|
|
if (cur && typeof cur === 'object' && key in (cur as Record<string, unknown>)) {
|
|
cur = (cur as Record<string, unknown>)[key];
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
return cur;
|
|
}
|