@rapidmx/web-client 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
- package/apps/shared/components/admin/settings/BrandingForm.tsx +398 -385
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
- package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
- package/apps/shared/components/layout/AppShell.tsx +288 -270
- package/apps/shared/components/mail/ConversationList.tsx +225 -33
- package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
- package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
- package/apps/shared/components/mail/MenuButton.tsx +384 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
- package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
- package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
- package/apps/shared/components/mail/compose/recipients.ts +104 -0
- package/apps/shared/components/mail/labelMenu.tsx +278 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +31 -4
- package/apps/shared/components/mail/listPreferences.ts +228 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -10
- package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
- package/apps/shared/plugins/pluginNav.ts +73 -0
- package/apps/shared/search/localIndexRpcClient.ts +3 -1
- package/apps/shared/styles/app.css +28 -5
- package/apps/www/index.tsx +1854 -1314
- package/apps/www/messages/[uid].tsx +5 -4
- package/dist/apps/admin/_404.d.ts +2 -0
- package/dist/apps/admin/_500.d.ts +2 -0
- package/dist/apps/admin/_layout.d.ts +8 -0
- package/dist/apps/admin/audit-log/index.d.ts +11 -0
- package/dist/apps/admin/branding/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
- package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
- package/dist/apps/admin/domains/[uid].d.ts +7 -0
- package/dist/apps/admin/domains/index.d.ts +3 -0
- package/dist/apps/admin/domains/new/index.d.ts +3 -0
- package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
- package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
- package/dist/apps/admin/index.d.ts +3 -0
- package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
- package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
- package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
- package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
- package/dist/apps/admin/plugins/index.d.ts +3 -0
- package/dist/apps/admin/quarantine/index.d.ts +6 -0
- package/dist/apps/admin/retention-policy/index.d.ts +3 -0
- package/dist/apps/admin/setup/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
- package/dist/apps/admin/transport-rules/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
- package/dist/apps/escrow/_layout.d.ts +10 -0
- package/dist/apps/escrow/audit-log/index.d.ts +9 -0
- package/dist/apps/escrow/index.d.ts +3 -0
- package/dist/apps/escrow/matters/[uid].d.ts +7 -0
- package/dist/apps/escrow/matters/new/index.d.ts +3 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +15 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +11 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
- package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
- package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
- package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
- package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
- package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
- package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
- package/dist/apps/shared/components/layout/AppShell.js +15 -13
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +47 -0
- package/dist/apps/shared/components/mail/ConversationList.js +84 -10
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +62 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
- package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
- package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
- package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
- package/dist/apps/shared/components/mail/MenuButton.js +211 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +101 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
- package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
- package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +73 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
- package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +43 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
- package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
- package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
- package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
- package/dist/apps/shared/components/mail/labelMenu.js +134 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +65 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +17 -5
- package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
- package/dist/apps/shared/components/mail/listPreferences.js +186 -0
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
- package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
- package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
- package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -10
- package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
- package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
- package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
- package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
- package/dist/apps/shared/plugins/pluginNav.js +33 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
- package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
- package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
- package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
- package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
- package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
- package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
- package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
- package/dist/apps/shared/search/searchTier2.d.ts +30 -0
- package/dist/apps/shared/styles/app.css +28 -5
- package/dist/apps/www/_404.d.ts +2 -0
- package/dist/apps/www/_500.d.ts +2 -0
- package/dist/apps/www/_layout.d.ts +14 -0
- package/dist/apps/www/calendar/index.d.ts +3 -0
- package/dist/apps/www/contacts/[uid].d.ts +13 -0
- package/dist/apps/www/contacts/index.d.ts +3 -0
- package/dist/apps/www/index.d.ts +3 -0
- package/dist/apps/www/index.js +506 -112
- package/dist/apps/www/messages/[uid].d.ts +11 -0
- package/dist/apps/www/messages/[uid].js +2 -3
- package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -0
- package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
- package/dist/apps/www/settings/filters/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
- package/dist/apps/www/settings/labels/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.d.ts +4 -0
- package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
- package/dist/apps/www/settings/sharing/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
- package/dist/apps/www/settings/signatures/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
- package/dist/apps/www/tasks/index.d.ts +3 -0
- package/package.json +3 -3
- package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
- package/apps/www/settings/booking-types/[uid].tsx +0 -306
- package/apps/www/settings/booking-types/index.tsx +0 -98
- package/apps/www/settings/booking-types/new/index.tsx +0 -212
- package/apps/www/settings/focused-inbox/index.tsx +0 -150
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
- package/dist/apps/www/settings/booking-types/[uid].js +0 -134
- package/dist/apps/www/settings/booking-types/index.js +0 -29
- package/dist/apps/www/settings/booking-types/new/index.js +0 -86
- package/dist/apps/www/settings/focused-inbox/index.js +0 -71
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
8
|
+
import LabelMenuButton from "./labelMenu.js";
|
|
9
|
+
import MoveToFolderDialog, { MOVE_TARGET_TYPES } from "./MoveToFolderDialog.js";
|
|
10
|
+
function actionClassName() {
|
|
11
|
+
return "px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
|
|
15
|
+
* select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
|
|
16
|
+
* archive, move, report junk and delete.
|
|
17
|
+
*
|
|
18
|
+
* "Delete" means *move to Deleted Items*, never a permanent erase: there is no bulk permanent-delete API,
|
|
19
|
+
* and the collection-level DELETE truncates the folder rather than removing a selection. An action whose
|
|
20
|
+
* target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
|
|
21
|
+
* of actions doesn't shift around between folders.
|
|
22
|
+
*/
|
|
23
|
+
export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, error, }) {
|
|
24
|
+
const selectedCount = totals?.selected ?? selected.length;
|
|
25
|
+
const listedCount = totals?.listed ?? listed.length;
|
|
26
|
+
// Counted on the rows that were ticked, but *emptied* on the messages: a ticked conversation whose
|
|
27
|
+
// messages are still being fetched has nothing for an action to act on yet.
|
|
28
|
+
const none = selectedCount === 0 || selected.length === 0;
|
|
29
|
+
const allSelected = listedCount > 0 && selectedCount === listedCount;
|
|
30
|
+
const currentType = folders.find((folder) => folder.uid === currentFolderUid)?.type;
|
|
31
|
+
const moveTargets = folders.filter((folder) => MOVE_TARGET_TYPES.has(folder.type) && folder.uid !== currentFolderUid);
|
|
32
|
+
// The same prompt the reading pane's own Move to opens, so one list of destinations and one way to
|
|
33
|
+
// create a folder serve both.
|
|
34
|
+
const [movePrompt, setMovePrompt] = useState(false);
|
|
35
|
+
// None of the three needs its folder to exist first - the caller creates or lazily provisions it (see
|
|
36
|
+
// `resolveFolderOfType()` and `bulkArchive()` in `apps/www/index.tsx`) - so the only thing that disables
|
|
37
|
+
// them is already being in the folder they would move to.
|
|
38
|
+
const archiveReason = currentType === "archive" ? "These messages are already in Archive" : undefined;
|
|
39
|
+
const junkReason = currentType === "junk" ? "These messages are already in Junk" : undefined;
|
|
40
|
+
const deleteReason = currentType === "deleted_items" ? "These messages are already in Deleted Items" : undefined;
|
|
41
|
+
// A label every selected message already carries starts ticked; one only some of them carry starts
|
|
42
|
+
// partially applied, and is left exactly as it is unless the reader touches that row.
|
|
43
|
+
const appliedToAll = labels.filter((label) => !none && selected.every((m) => m.labelUids?.includes(label.uid)));
|
|
44
|
+
const appliedToSome = labels.filter((label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label));
|
|
45
|
+
return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy, note: selected.length === 1
|
|
46
|
+
? "Ticked labels are applied, unticked ones removed."
|
|
47
|
+
: "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, title: deleteReason, className: actionClassName(), children: "Delete" })] }), error && (_jsx("div", { className: "px-3 pb-2", children: _jsx(Alert, { children: error }) }))] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
/** What every row has. `checked` turns it into a radio/checkbox item (the caller says which via `role`) and
|
|
3
|
+
* draws the checkmark column; without it the row is a plain command. */
|
|
4
|
+
interface MenuItemBase {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** A one-line explanation under the label, for an item whose effect isn't obvious from its name. */
|
|
8
|
+
description?: string;
|
|
9
|
+
role?: "menuitem" | "menuitemradio" | "menuitemcheckbox";
|
|
10
|
+
/** `"mixed"` is ARIA's third checkbox state - some of the things this row acts on have it and some
|
|
11
|
+
* don't, which is what a label applied to only part of a multi-message selection looks like. */
|
|
12
|
+
checked?: boolean | "mixed";
|
|
13
|
+
/** A colour swatch before the label - a label's own colour. */
|
|
14
|
+
swatchColor?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Leaves the menu open after choosing this row, for a multi-select list where several rows are ticked
|
|
17
|
+
* before one command commits them all. */
|
|
18
|
+
keepOpen?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** A row that does something when chosen. */
|
|
21
|
+
export interface MenuCommandSpec extends MenuItemBase {
|
|
22
|
+
onSelect: () => void;
|
|
23
|
+
submenu?: never;
|
|
24
|
+
}
|
|
25
|
+
/** A row that opens a submenu instead: choosing it replaces the menu's contents with these sections, under
|
|
26
|
+
* a Back row. One level deep, which is all any menu here needs - so it has nothing of its own to do. */
|
|
27
|
+
export interface MenuSubmenuSpec extends MenuItemBase {
|
|
28
|
+
submenu: MenuSectionSpec[];
|
|
29
|
+
onSelect?: never;
|
|
30
|
+
}
|
|
31
|
+
export type MenuItemSpec = MenuCommandSpec | MenuSubmenuSpec;
|
|
32
|
+
/** A labelled group of items. Groups after the first are drawn with a separator above them. */
|
|
33
|
+
export interface MenuSectionSpec {
|
|
34
|
+
key: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
/** A short note under the group's items - e.g. why some of them are unavailable right now. */
|
|
37
|
+
note?: string;
|
|
38
|
+
items: MenuItemSpec[];
|
|
39
|
+
}
|
|
40
|
+
/** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
|
|
41
|
+
export declare function menuHeight(sections: MenuSectionSpec[], width?: number): number;
|
|
42
|
+
export interface MenuButtonProps {
|
|
43
|
+
/** The trigger's visible label. */
|
|
44
|
+
label: ReactNode;
|
|
45
|
+
/** The trigger's accessible name, which also names the menu itself - includes the current selection
|
|
46
|
+
* where there is one ("Filter: Unread"), so the button says what it's set to, not just what it does. */
|
|
47
|
+
"aria-label": string;
|
|
48
|
+
icon?: ReactNode;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
title?: string;
|
|
51
|
+
sections: MenuSectionSpec[];
|
|
52
|
+
width?: number;
|
|
53
|
+
/** Extra classes for the trigger, on top of the shared toolbar-button styling. */
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Told whenever the menu opens or closes - how a caller resets a draft it keeps for the menu's own
|
|
56
|
+
* multi-select rows (see `useLabelDraft()`). */
|
|
57
|
+
onOpenChange?: (open: boolean) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A toolbar button that opens an ARIA menu: `role="menu"` rows with checkmarks for the current choice,
|
|
61
|
+
* full keyboard support (Enter/Space or Arrow Down to open, arrows/Home/End to move with wrap, Enter/Space
|
|
62
|
+
* to choose, Escape or Tab to close with focus returning to the trigger) and a click outside to dismiss.
|
|
63
|
+
*
|
|
64
|
+
* Rendered through `PopoverPortal` rather than as an absolutely-positioned child: the mail list toolbar
|
|
65
|
+
* lives inside the list's own `overflow-y-auto` scroll container, which clips an absolute popup however
|
|
66
|
+
* high its `z-index` - the same reason Compose's own pickers portal out (see `PopoverPortal`'s own doc
|
|
67
|
+
* comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
|
|
68
|
+
* semantics, roving focus and keyboard handling are this component's own.
|
|
69
|
+
*/
|
|
70
|
+
export default function MenuButton({ label, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { HiCheck, HiChevronDown, HiChevronLeft, HiChevronRight, HiMinus } from "react-icons/hi2";
|
|
8
|
+
import PopoverPortal from "@rapidmx/react-shared/components/overlays/PopoverPortal.js";
|
|
9
|
+
// `PopoverPortal` positions a fixed-size box (it has no auto-height mode), so the menu's height is
|
|
10
|
+
// computed from its own contents rather than measured - from whichever level is *shown*, so a submenu
|
|
11
|
+
// isn't left standing in the parent menu's taller box. These are the exact heights the classes below
|
|
12
|
+
// render at, so the box is never short enough to clip its last row: an item is `h-9`, a group label
|
|
13
|
+
// `h-6`, a note as many `leading-4` lines as it wraps to, a separator a 1px rule inside `my-1`, and the
|
|
14
|
+
// list itself `py-1`.
|
|
15
|
+
const ITEM_HEIGHT = 36;
|
|
16
|
+
const ITEM_DESCRIPTION_HEIGHT = 16;
|
|
17
|
+
const GROUP_LABEL_HEIGHT = 24;
|
|
18
|
+
const NOTE_LINE_HEIGHT = 16;
|
|
19
|
+
const NOTE_PADDING = 4;
|
|
20
|
+
/** A note wraps, so its height depends on how much of it fits a line: `text-xs` averages a little over 6px
|
|
21
|
+
* a character, and the note sits inside the list's `px-3`. Rounded so the estimate is never *under* the
|
|
22
|
+
* lines the browser actually draws - a box a few pixels too tall shows blank space, one too short clips. */
|
|
23
|
+
const NOTE_CHAR_WIDTH = 6.4;
|
|
24
|
+
const NOTE_PADDING_X = 24;
|
|
25
|
+
const SEPARATOR_HEIGHT = 9;
|
|
26
|
+
const LIST_PADDING = 8;
|
|
27
|
+
const MENU_MAX_HEIGHT = 460;
|
|
28
|
+
const DEFAULT_MENU_WIDTH = 248;
|
|
29
|
+
/** How tall `note` renders at `width`, wrapped. */
|
|
30
|
+
function noteHeight(note, width) {
|
|
31
|
+
const charsPerLine = Math.max(1, Math.floor((width - NOTE_PADDING_X) / NOTE_CHAR_WIDTH));
|
|
32
|
+
return Math.ceil(note.length / charsPerLine) * NOTE_LINE_HEIGHT + NOTE_PADDING;
|
|
33
|
+
}
|
|
34
|
+
/** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
|
|
35
|
+
export function menuHeight(sections, width = DEFAULT_MENU_WIDTH) {
|
|
36
|
+
let height = LIST_PADDING;
|
|
37
|
+
sections.forEach((section, index) => {
|
|
38
|
+
if (index > 0) {
|
|
39
|
+
height += SEPARATOR_HEIGHT;
|
|
40
|
+
}
|
|
41
|
+
if (section.label) {
|
|
42
|
+
height += GROUP_LABEL_HEIGHT;
|
|
43
|
+
}
|
|
44
|
+
for (const item of section.items) {
|
|
45
|
+
height += ITEM_HEIGHT + (item.description ? ITEM_DESCRIPTION_HEIGHT : 0);
|
|
46
|
+
}
|
|
47
|
+
if (section.note) {
|
|
48
|
+
height += noteHeight(section.note, width);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return Math.min(height, MENU_MAX_HEIGHT);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A toolbar button that opens an ARIA menu: `role="menu"` rows with checkmarks for the current choice,
|
|
55
|
+
* full keyboard support (Enter/Space or Arrow Down to open, arrows/Home/End to move with wrap, Enter/Space
|
|
56
|
+
* to choose, Escape or Tab to close with focus returning to the trigger) and a click outside to dismiss.
|
|
57
|
+
*
|
|
58
|
+
* Rendered through `PopoverPortal` rather than as an absolutely-positioned child: the mail list toolbar
|
|
59
|
+
* lives inside the list's own `overflow-y-auto` scroll container, which clips an absolute popup however
|
|
60
|
+
* high its `z-index` - the same reason Compose's own pickers portal out (see `PopoverPortal`'s own doc
|
|
61
|
+
* comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
|
|
62
|
+
* semantics, roving focus and keyboard handling are this component's own.
|
|
63
|
+
*/
|
|
64
|
+
export default function MenuButton({ label, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
|
|
65
|
+
const [open, setOpen] = useState(false);
|
|
66
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
67
|
+
// The key of the item whose submenu is showing, or `null` at the top level. Held as a key, not as the
|
|
68
|
+
// item itself, because `sections` is rebuilt on every render.
|
|
69
|
+
const [submenuKey, setSubmenuKey] = useState(null);
|
|
70
|
+
const triggerRef = useRef(null);
|
|
71
|
+
const itemRefs = useRef([]);
|
|
72
|
+
// State, not a ref: `PopoverPortal` renders nothing at all until its own positioning effect has run, so
|
|
73
|
+
// the rows don't exist yet when this component's effects first fire. Keying the focus effect on the
|
|
74
|
+
// menu node itself makes it run again once they do.
|
|
75
|
+
const [menuNode, setMenuNode] = useState(null);
|
|
76
|
+
const openSubmenu = sections
|
|
77
|
+
.flatMap((section) => section.items)
|
|
78
|
+
.find((item) => item.key === submenuKey && !!item.submenu);
|
|
79
|
+
/** The row that leaves a submenu again - synthesized rather than asked of the caller, so every submenu
|
|
80
|
+
* has the same way back however it was built. */
|
|
81
|
+
const backItem = {
|
|
82
|
+
key: "__back",
|
|
83
|
+
label: `Back to ${ariaLabel}`,
|
|
84
|
+
keepOpen: true,
|
|
85
|
+
onSelect: () => leaveSubmenu(),
|
|
86
|
+
};
|
|
87
|
+
const shownSections = openSubmenu
|
|
88
|
+
? [{ key: "__back", items: [backItem] }, ...openSubmenu.submenu]
|
|
89
|
+
: sections;
|
|
90
|
+
const items = shownSections.flatMap((section) => section.items);
|
|
91
|
+
const enabledIndexes = items.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index !== -1);
|
|
92
|
+
/** The row focus starts on: the current choice where there is one, else the first row that can take focus. */
|
|
93
|
+
function initialActiveIndex(within) {
|
|
94
|
+
const checked = within.findIndex((item) => item.checked === true && !item.disabled);
|
|
95
|
+
const enabled = within.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index !== -1);
|
|
96
|
+
return checked === -1 ? (enabled[0] ?? 0) : checked;
|
|
97
|
+
}
|
|
98
|
+
function openMenu() {
|
|
99
|
+
setSubmenuKey(null);
|
|
100
|
+
setActiveIndex(initialActiveIndex(sections.flatMap((section) => section.items)));
|
|
101
|
+
setOpen(true);
|
|
102
|
+
onOpenChange?.(true);
|
|
103
|
+
}
|
|
104
|
+
function closeMenu(returnFocus) {
|
|
105
|
+
setOpen(false);
|
|
106
|
+
setSubmenuKey(null);
|
|
107
|
+
onOpenChange?.(false);
|
|
108
|
+
if (returnFocus) {
|
|
109
|
+
triggerRef.current?.focus();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function enterSubmenu(item) {
|
|
113
|
+
setSubmenuKey(item.key);
|
|
114
|
+
// Past the Back row, onto the first row of the submenu itself.
|
|
115
|
+
setActiveIndex(1 + initialActiveIndex(item.submenu.flatMap((section) => section.items)));
|
|
116
|
+
}
|
|
117
|
+
function leaveSubmenu() {
|
|
118
|
+
// Back on the row the submenu was opened from.
|
|
119
|
+
const parentIndex = sections.flatMap((section) => section.items).findIndex((item) => item.key === submenuKey);
|
|
120
|
+
setSubmenuKey(null);
|
|
121
|
+
setActiveIndex(parentIndex);
|
|
122
|
+
}
|
|
123
|
+
/** What a row does when it is chosen: open its submenu, or run it and close unless it asked to stay. */
|
|
124
|
+
function selectItem(item) {
|
|
125
|
+
if (item.submenu) {
|
|
126
|
+
enterSubmenu(item);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
item.onSelect();
|
|
130
|
+
if (!item.keepOpen) {
|
|
131
|
+
closeMenu(true);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Roving focus: only the active row is tabbable, and it takes DOM focus whenever it changes, so the
|
|
135
|
+
// arrow keys move the screen reader's cursor too rather than only a visual highlight.
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (open && menuNode) {
|
|
138
|
+
// A disabled row can't take focus, so a menu with nothing enabled focuses its own container -
|
|
139
|
+
// otherwise the keys it handles would go to whatever had focus before it opened.
|
|
140
|
+
if (enabledIndexes.length === 0) {
|
|
141
|
+
menuNode.focus();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
itemRefs.current[activeIndex]?.focus();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// `submenuKey` too: drilling in or out can land on the same index in the other level's list, and
|
|
148
|
+
// the row that index *means* is a different button, which the effect must move focus to.
|
|
149
|
+
}, [open, activeIndex, submenuKey, menuNode]);
|
|
150
|
+
function moveActive(delta) {
|
|
151
|
+
if (enabledIndexes.length === 0) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const position = enabledIndexes.indexOf(activeIndex);
|
|
155
|
+
setActiveIndex(enabledIndexes[(position + delta + enabledIndexes.length) % enabledIndexes.length]);
|
|
156
|
+
}
|
|
157
|
+
function handleMenuKeyDown(e) {
|
|
158
|
+
if (e.key === "ArrowDown") {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
moveActive(1);
|
|
161
|
+
}
|
|
162
|
+
else if (e.key === "ArrowUp") {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
moveActive(-1);
|
|
165
|
+
}
|
|
166
|
+
else if (e.key === "Home") {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
setActiveIndex(enabledIndexes[0] ?? 0);
|
|
169
|
+
}
|
|
170
|
+
else if (e.key === "End") {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
setActiveIndex(enabledIndexes[enabledIndexes.length - 1] ?? 0);
|
|
173
|
+
}
|
|
174
|
+
else if (e.key === "ArrowRight" && items[activeIndex]?.submenu) {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
enterSubmenu(items[activeIndex]);
|
|
177
|
+
}
|
|
178
|
+
else if ((e.key === "ArrowLeft" || e.key === "Escape") && openSubmenu) {
|
|
179
|
+
// ARIA's own submenu behaviour: Escape leaves the submenu for its parent menu rather than
|
|
180
|
+
// dismissing the whole thing.
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
leaveSubmenu();
|
|
183
|
+
}
|
|
184
|
+
else if (e.key === "Escape" || e.key === "Tab") {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
closeMenu(true);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function handleTriggerKeyDown(e) {
|
|
190
|
+
if (!open && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
openMenu();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let itemIndex = -1;
|
|
196
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": ariaLabel, title: title, disabled: disabled, onClick: () => (open ? closeMenu(false) : openMenu()), onKeyDown: handleTriggerKeyDown, className: [
|
|
197
|
+
// `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
|
|
198
|
+
// whatever sits beside it in the toolbar off the edge of a 384px list column.
|
|
199
|
+
"inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
|
|
200
|
+
className,
|
|
201
|
+
].join(" "), children: [icon, _jsx("span", { className: "truncate", children: label }), _jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: "shrink-0 text-text-muted" })] }), open && (_jsx(PopoverPortal, { anchorRef: triggerRef, onClose: () => closeMenu(false), width: width, height: menuHeight(shownSections, width), "aria-label": ariaLabel, children: _jsx("div", { ref: setMenuNode, role: "menu", tabIndex: -1, "aria-label": ariaLabel, onKeyDown: handleMenuKeyDown, className: "flex-1 overflow-y-auto py-1", children: shownSections.map((section, sectionIndex) => (_jsxs("div", { role: "group", "aria-label": section.label, className: sectionIndex > 0 ? "border-t border-border mt-1 pt-1" : "", children: [section.label && (_jsx("div", { "aria-hidden": "true", className: "h-6 flex items-center px-3 text-xs font-semibold uppercase tracking-wide text-text-muted", children: section.label })), section.items.map((item) => {
|
|
202
|
+
itemIndex += 1;
|
|
203
|
+
const index = itemIndex;
|
|
204
|
+
const role = item.role ?? "menuitem";
|
|
205
|
+
return (_jsxs("button", { ref: (node) => {
|
|
206
|
+
itemRefs.current[index] = node;
|
|
207
|
+
}, type: "button", role: role, disabled: item.disabled, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
|
|
208
|
+
? {}
|
|
209
|
+
: { "aria-checked": item.checked === "mixed" ? "mixed" : !!item.checked }), ...(item.submenu ? { "aria-haspopup": "menu", "aria-expanded": false } : {}), onClick: () => selectItem(item), className: "w-full flex items-start gap-2 px-3 py-2 text-left text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsxs("span", { className: "w-4 shrink-0 flex justify-center pt-0.5", children: [item.key === "__back" && (_jsx(HiChevronLeft, { size: 14, "aria-hidden": "true", className: "text-text-muted" })), item.checked === "mixed" ? (_jsx(HiMinus, { size: 14, "aria-hidden": "true", className: "text-primary-dark" })) : (item.checked && _jsx(HiCheck, { size: 14, "aria-hidden": "true", className: "text-primary-dark" }))] }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex h-5 items-center gap-1.5", children: [item.swatchColor && (_jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: item.swatchColor } })), _jsx("span", { className: "truncate", children: item.label })] }), item.description && (_jsx("span", { className: "block h-4 text-xs leading-4 text-text-muted truncate font-normal", children: item.description }))] }), item.submenu && (_jsx(HiChevronRight, { size: 14, "aria-hidden": "true", className: "shrink-0 mt-0.5 text-text-muted" }))] }, item.key));
|
|
210
|
+
}), section.note && _jsx("p", { className: "px-3 py-1 text-xs leading-4 text-text-muted", children: section.note })] }, section.key))) }) }))] }));
|
|
211
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Attachment, Folder, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
|
|
4
|
+
import { MessageSecurityResult } from "@rapidmx/react-shared/crypto/messageSecurity.js";
|
|
5
|
+
/** The detail line under a `verified_at_first_open` badge. */
|
|
6
|
+
export declare function verifiedAtFirstOpenMessage(result: MessageSecurityResult): string;
|
|
7
|
+
export interface SenderNameCheck {
|
|
8
|
+
/** The display name contains an `@` or a look-alike - it reads as (part of) an address. */
|
|
9
|
+
looksLikeAddress: boolean;
|
|
10
|
+
/** It shows an address-shaped token that isn't `actualAddress` - e.g. `"ceo@corp.com" <x@corp-pay.com>`. */
|
|
11
|
+
misleading: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Whether a sender's display name poses as an address, and whether that address differs from the one actually sent
|
|
14
|
+
* from. Compatibility forms are folded first (NFKC turns the fullwidth and small @ into `@`), so a look-alike can't
|
|
15
|
+
* hide the token. */
|
|
16
|
+
export declare function checkSenderName(displayName: string | undefined, actualAddress: string): SenderNameCheck;
|
|
17
|
+
export declare const TRUST_SIGNER_CONFLICT_MESSAGE = "A different signing key is already trusted for this sender, so this one wasn't trusted. The sender's key may have changed; confirm with them before doing anything.";
|
|
18
|
+
export declare const TRUST_SIGNER_INVALID_MESSAGE = "This certificate can't be trusted for this sender.";
|
|
19
|
+
export declare const TRUST_SIGNER_FORBIDDEN_MESSAGE = "You don't have permission to trust signers for this mailbox.";
|
|
20
|
+
export declare const TRUST_SIGNER_GENERIC_MESSAGE = "Couldn't trust this signer. Try again.";
|
|
21
|
+
export declare const KEPT_CURRENT_SIGNING_KEY_MESSAGE = "You kept the current signing key for this sender. This message stays unverified.";
|
|
22
|
+
/** The error text for a failed `trustSigner()`. */
|
|
23
|
+
export declare function trustSignerErrorMessage(err: unknown): string;
|
|
24
|
+
/** Groups a fingerprint into 4-character blocks (`AB12 CD34 ...`), ignoring any separators it came with, so it can be
|
|
25
|
+
* read out and compared with the sender over another channel. */
|
|
26
|
+
export declare function formatFingerprint(fingerprint: string): string;
|
|
27
|
+
/** Sanitizes a client-rendered body for `srcDoc` (DOMPurify plus every remote resource reference stripped - see
|
|
28
|
+
* react-shared's `messageBodySanitizer.ts`), with the CSP meta as its very first element. */
|
|
29
|
+
export declare function buildSecureSrcDoc(html: string): string;
|
|
30
|
+
export interface MessageDetailPaneProps {
|
|
31
|
+
message: Message | null;
|
|
32
|
+
attachments: Attachment[];
|
|
33
|
+
/** Present only on the mobile detail route — renders a "back to messages" link above the header. Absent
|
|
34
|
+
* on the desktop reading pane, which never navigates away (selecting a different message just swaps
|
|
35
|
+
* `message` in place). */
|
|
36
|
+
backHref?: string;
|
|
37
|
+
/** Whether `message` currently lives in Sent Items — the only folder recall is offered from, matching
|
|
38
|
+
* Outlook's own restriction (and `BaseMessageRoute.recall()`'s own server-side check). Each caller
|
|
39
|
+
* computes this from its own already-loaded folder list rather than this component fetching folders
|
|
40
|
+
* itself. */
|
|
41
|
+
isSentItems?: boolean;
|
|
42
|
+
/** Called with the server's updated copy (carrying `recallRequestedAt`) after a successful recall, so
|
|
43
|
+
* the caller can patch its own in-memory message/list state — mirrors `mailDetailHooks.ts`'s
|
|
44
|
+
* `useMarkMessageRead`'s identical `onUpdated` callback. */
|
|
45
|
+
onRecalled?: (updated: Message) => void;
|
|
46
|
+
/** Whether `message` currently lives in Outbox — the only folder a scheduled send can be canceled
|
|
47
|
+
* from. Each caller computes this the same way it computes `isSentItems`. */
|
|
48
|
+
isOutbox?: boolean;
|
|
49
|
+
/** The mailbox's Drafts folder uid — required when `isOutbox` is `true`, since canceling a
|
|
50
|
+
* scheduled send moves the message back into Drafts (see `cancelScheduledSend()`'s own doc comment
|
|
51
|
+
* on why clearing `scheduledSendTime` alone doesn't do that). */
|
|
52
|
+
draftsFolderUid?: string;
|
|
53
|
+
/** Called with the server's updated copy (now back in Drafts, `scheduledSendTime` cleared) after
|
|
54
|
+
* successfully canceling a scheduled send. */
|
|
55
|
+
onScheduledSendCanceled?: (updated: Message) => void;
|
|
56
|
+
/** Every folder of this message's own mailbox, for the Move to prompt's destination list. Absent or
|
|
57
|
+
* empty simply hides the control — there is nowhere to move to. Each caller already resolves this to
|
|
58
|
+
* work out `isSentItems`/`isOutbox`/`draftsFolderUid`, so nothing new is fetched for it. */
|
|
59
|
+
folders?: Folder[];
|
|
60
|
+
/** Called with the server's updated copy (now in the chosen folder) after a successful Move to. A move
|
|
61
|
+
* takes the message out of whichever folder is being listed, so callers remove it from their list
|
|
62
|
+
* rather than patching it — unlike `onClassified`, which this replaces. */
|
|
63
|
+
onMoved?: (updated: Message) => void;
|
|
64
|
+
/** A folder created from the Move to prompt, so the caller's own list and the folder sidebar pick it up
|
|
65
|
+
* without a page load. */
|
|
66
|
+
onFolderCreated?: (folder: Folder) => void;
|
|
67
|
+
/** Called with the server's updated copy after approving/declining a pending delivery/read receipt —
|
|
68
|
+
* mirrors `onRecalled`'s identical shape. No gating prop needed (unlike `isSentItems`/`isOutbox`):
|
|
69
|
+
* `deliveryReceiptPending`/`readReceiptPending` already live directly on `message` and are only ever
|
|
70
|
+
* `true` on a real delivered copy, so the banner below is self-gating. */
|
|
71
|
+
onReceiptHandled?: (updated: Message) => void;
|
|
72
|
+
/** Called with the server's updated copy (now filed under the mailbox's Archive folder) after a
|
|
73
|
+
* successful archive — mirrors `onRecalled`'s identical shape. Archiving itself is offered for any
|
|
74
|
+
* message except one currently in Drafts or Outbox (mirrors `BaseMessageRoute.archive()`'s own
|
|
75
|
+
* server-side 400 guard); Drafts is detected via `draftsFolderUid` (already passed by every caller
|
|
76
|
+
* for the Outbox-cancel flow) rather than a new prop. */
|
|
77
|
+
onArchived?: (updated: Message) => void;
|
|
78
|
+
/** Every label defined in this message's mailbox, for the label-assignment popover below — each
|
|
79
|
+
* caller fetches its own mailbox's labels the same way it already resolves `draftsFolderUid`
|
|
80
|
+
* (`listLabels()`). Absent/empty simply hides the Labels control - there's nothing to assign. */
|
|
81
|
+
labels?: Label[];
|
|
82
|
+
/** Called with the server's updated copy (carrying the new `labelUids`) after successfully toggling
|
|
83
|
+
* a label — always patches in place, never removes from a caller's list (unlike `onArchived`):
|
|
84
|
+
* changing labels never moves a message between folders. */
|
|
85
|
+
onLabelsChanged?: (updated: Message) => void;
|
|
86
|
+
/** Handed a label created from the Labels menu, for the caller to add to `labels`. Without it the menu
|
|
87
|
+
* offers no "New label" row, only the link to where labels are managed. */
|
|
88
|
+
onLabelCreated?: (label: Label) => void;
|
|
89
|
+
/** Rendered as one message of a thread (`ConversationThreadPane`) rather than as the pane itself: the
|
|
90
|
+
* subject becomes a heading under the thread's own, since a document has one `h1` and the thread's is
|
|
91
|
+
* the conversation. Everything else - the badges, the actions, the body - is identical. */
|
|
92
|
+
inThread?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A message's reading pane — header (subject/from/to/attachments) plus a sandboxed iframe for the body.
|
|
96
|
+
* Shared by the desktop inline pane (`apps/www/index.tsx`, always visible alongside the message list),
|
|
97
|
+
* and the mobile detail route (`apps/www/messages/[uid].tsx`, a full page on its own reached by tapping
|
|
98
|
+
* a message row) — see each call site for how `message`/`attachments`/`isSentItems` are sourced. The
|
|
99
|
+
* desktop pane shows whichever message the list opened, including one opened from a conversation row.
|
|
100
|
+
*/
|
|
101
|
+
export default function MessageDetailPane(props: MessageDetailPaneProps): React.JSX.Element;
|