@rapidmx/web-client 0.16.0 → 0.17.2

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.
Files changed (184) hide show
  1. package/README.md +36 -28
  2. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  3. package/apps/admin/domains/[uid].tsx +3 -1
  4. package/apps/admin/domains/new/index.tsx +3 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +3 -1
  6. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  7. package/apps/admin/index.tsx +3 -1
  8. package/apps/admin/mailboxes/[uid].tsx +3 -1
  9. package/apps/admin/mailboxes/new/index.tsx +3 -1
  10. package/apps/admin/transport-rules/new/index.tsx +3 -1
  11. package/apps/escrow/matters/new/index.tsx +3 -1
  12. package/apps/shared/components/admin/layout/AdminShell.tsx +4 -1
  13. package/apps/shared/components/admin/setup/SetupWizard.tsx +3 -1
  14. package/apps/shared/components/calendar/layout/CalendarShell.tsx +2 -2
  15. package/apps/shared/components/contacts/layout/ContactsShell.tsx +2 -2
  16. package/apps/shared/components/layout/AppShell.tsx +24 -43
  17. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -0
  18. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  19. package/apps/shared/components/mail/MailSelectionBar.tsx +19 -15
  20. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  21. package/apps/shared/components/mail/MessageDetailPane.tsx +202 -4
  22. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  23. package/apps/shared/components/mail/layout/MailShell.tsx +36 -34
  24. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  25. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  26. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  27. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  28. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  29. package/apps/shared/components/settings/layout/SettingsShell.tsx +3 -2
  30. package/apps/shared/components/tasks/layout/TasksShell.tsx +2 -2
  31. package/apps/shared/keyboard/GlobalShortcuts.tsx +1 -1
  32. package/apps/shared/mail/folderOfType.ts +49 -0
  33. package/apps/shared/mail/permanentDelete.ts +160 -0
  34. package/apps/shared/mail/reportNotices.ts +66 -0
  35. package/apps/shared/mail/senderBlocking.ts +141 -0
  36. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  37. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  38. package/apps/shared/navigation/frameContext.tsx +1 -1
  39. package/apps/shared/navigation/index.ts +70 -0
  40. package/apps/shared/navigation/pageTitle.ts +19 -0
  41. package/apps/www/_shell.tsx +82 -0
  42. package/apps/www/calendar/index.tsx +5 -2
  43. package/apps/www/contacts/[uid].tsx +6 -3
  44. package/apps/www/contacts/index.tsx +8 -3
  45. package/apps/www/index.tsx +146 -31
  46. package/apps/www/messages/[uid].tsx +8 -4
  47. package/apps/www/settings/appearance/index.tsx +5 -2
  48. package/apps/www/settings/auto-reply/index.tsx +5 -2
  49. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  50. package/apps/www/settings/encryption/index.tsx +5 -2
  51. package/apps/www/settings/filters/[uid].tsx +5 -2
  52. package/apps/www/settings/filters/index.tsx +5 -2
  53. package/apps/www/settings/filters/new/index.tsx +30 -5
  54. package/apps/www/settings/labels/index.tsx +5 -2
  55. package/apps/www/settings/privacy/index.tsx +5 -2
  56. package/apps/www/settings/profile/index.tsx +5 -2
  57. package/apps/www/settings/read-receipts/index.tsx +5 -2
  58. package/apps/www/settings/sharing/index.tsx +5 -2
  59. package/apps/www/settings/signatures/[uid].tsx +5 -2
  60. package/apps/www/settings/signatures/index.tsx +5 -2
  61. package/apps/www/settings/signatures/new/index.tsx +6 -3
  62. package/apps/www/tasks/index.tsx +7 -2
  63. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  64. package/dist/apps/admin/domains/[uid].js +3 -1
  65. package/dist/apps/admin/domains/new/index.js +3 -1
  66. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  67. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  68. package/dist/apps/admin/index.js +3 -1
  69. package/dist/apps/admin/mailboxes/[uid].js +3 -1
  70. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  71. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  72. package/dist/apps/escrow/matters/new/index.js +3 -1
  73. package/dist/apps/shared/components/admin/layout/AdminShell.js +4 -1
  74. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  75. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  76. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  77. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  78. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  79. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  80. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  81. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  82. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +9 -4
  83. package/dist/apps/shared/components/mail/MailSelectionBar.js +10 -12
  84. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  85. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  86. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  87. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  88. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  89. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  90. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +13 -9
  91. package/dist/apps/shared/components/mail/layout/MailShell.js +26 -33
  92. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  93. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  94. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  95. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  96. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  97. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  98. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  99. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  100. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  101. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  102. package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
  103. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  104. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  105. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  106. package/dist/apps/shared/mail/folderOfType.js +40 -0
  107. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  108. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  109. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  110. package/dist/apps/shared/mail/reportNotices.js +47 -0
  111. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  112. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  113. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  114. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  115. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  116. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  117. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  118. package/dist/apps/shared/navigation/index.d.ts +27 -0
  119. package/dist/apps/shared/navigation/index.js +58 -0
  120. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  121. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  122. package/dist/apps/www/_shell.d.ts +26 -0
  123. package/dist/apps/www/_shell.js +31 -0
  124. package/dist/apps/www/calendar/index.d.ts +7 -2
  125. package/dist/apps/www/calendar/index.js +4 -2
  126. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  127. package/dist/apps/www/contacts/[uid].js +5 -3
  128. package/dist/apps/www/contacts/index.d.ts +7 -2
  129. package/dist/apps/www/contacts/index.js +6 -4
  130. package/dist/apps/www/index.d.ts +7 -2
  131. package/dist/apps/www/index.js +117 -19
  132. package/dist/apps/www/messages/[uid].d.ts +14 -3
  133. package/dist/apps/www/messages/[uid].js +7 -5
  134. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  135. package/dist/apps/www/settings/appearance/index.js +4 -2
  136. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  137. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  138. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  139. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  140. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  141. package/dist/apps/www/settings/encryption/index.js +4 -2
  142. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  143. package/dist/apps/www/settings/filters/[uid].js +4 -2
  144. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  145. package/dist/apps/www/settings/filters/index.js +4 -2
  146. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  147. package/dist/apps/www/settings/filters/new/index.js +28 -5
  148. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  149. package/dist/apps/www/settings/labels/index.js +4 -2
  150. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  151. package/dist/apps/www/settings/privacy/index.js +4 -2
  152. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  153. package/dist/apps/www/settings/profile/index.js +4 -2
  154. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  155. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  156. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  157. package/dist/apps/www/settings/sharing/index.js +4 -2
  158. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  159. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  160. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  161. package/dist/apps/www/settings/signatures/index.js +4 -2
  162. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  163. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  164. package/dist/apps/www/tasks/index.d.ts +7 -2
  165. package/dist/apps/www/tasks/index.js +5 -3
  166. package/package.json +4 -2
  167. package/apps/shared/navigation/AppRouter.tsx +0 -300
  168. package/apps/shared/navigation/idle.ts +0 -45
  169. package/apps/shared/navigation/routerContext.tsx +0 -83
  170. package/apps/shared/navigation/routes.ts +0 -72
  171. package/apps/www/_routedPage.tsx +0 -24
  172. package/apps/www/_routes.ts +0 -37
  173. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  174. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  175. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  176. package/dist/apps/shared/navigation/idle.js +0 -43
  177. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  178. package/dist/apps/shared/navigation/routerContext.js +0 -56
  179. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  180. package/dist/apps/shared/navigation/routes.js +0 -37
  181. package/dist/apps/www/_routedPage.d.ts +0 -12
  182. package/dist/apps/www/_routedPage.js +0 -19
  183. package/dist/apps/www/_routes.d.ts +0 -11
  184. package/dist/apps/www/_routes.js +0 -31
@@ -12,6 +12,10 @@ interface MenuItemBase {
12
12
  checked?: boolean | "mixed";
13
13
  /** A colour swatch before the label - a label's own colour. */
14
14
  swatchColor?: string;
15
+ /** An icon before the label, for a menu whose rows are commands (the reading pane's "More actions"). */
16
+ icon?: ReactNode;
17
+ /** A tooltip for the row - the full text of a label the row truncates. */
18
+ title?: string;
15
19
  disabled?: boolean;
16
20
  /** Leaves the menu open after choosing this row, for a multi-select list where several rows are ticked
17
21
  * before one command commits them all. */
@@ -40,8 +44,11 @@ export interface MenuSectionSpec {
40
44
  /** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
41
45
  export declare function menuHeight(sections: MenuSectionSpec[], width?: number): number;
42
46
  export interface MenuButtonProps {
43
- /** The trigger's visible label. */
47
+ /** The trigger's visible label. Not drawn (nor is the chevron) with `iconOnly`, where `aria-label` is all the trigger says. */
44
48
  label: ReactNode;
49
+ /** The trigger is just its `icon` - a round icon button like the reading pane's own command row, whose `className` it takes over
50
+ * completely. `aria-label` (and `title`) name it. */
51
+ iconOnly?: boolean;
45
52
  /** The trigger's accessible name, which also names the menu itself - includes the current selection
46
53
  * where there is one ("Filter: Unread"), so the button says what it's set to, not just what it does. */
47
54
  "aria-label": string;
@@ -67,5 +74,5 @@ export interface MenuButtonProps {
67
74
  * comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
68
75
  * semantics, roving focus and keyboard handling are this component's own.
69
76
  */
70
- export default function MenuButton({ label, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
77
+ export default function MenuButton({ label, iconOnly, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
71
78
  export {};
@@ -61,7 +61,7 @@ export function menuHeight(sections, width = DEFAULT_MENU_WIDTH) {
61
61
  * comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
62
62
  * semantics, roving focus and keyboard handling are this component's own.
63
63
  */
64
- export default function MenuButton({ label, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
64
+ export default function MenuButton({ label, iconOnly = false, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
65
65
  const [open, setOpen] = useState(false);
66
66
  const [activeIndex, setActiveIndex] = useState(0);
67
67
  // The key of the item whose submenu is showing, or `null` at the top level. Held as a key, not as the
@@ -111,8 +111,10 @@ export default function MenuButton({ label, icon, disabled, title, sections, wid
111
111
  }
112
112
  function enterSubmenu(item) {
113
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)));
114
+ const rows = item.submenu.flatMap((section) => section.items);
115
+ // Past the Back row, onto the first row of the submenu itself - or, when none of them can take focus (every row is unavailable right now), onto
116
+ // the Back row, so the keyboard still has a place to be.
117
+ setActiveIndex(rows.some((row) => !row.disabled) ? 1 + initialActiveIndex(rows) : 0);
116
118
  }
117
119
  function leaveSubmenu() {
118
120
  // Back on the row the submenu was opened from.
@@ -193,19 +195,21 @@ export default function MenuButton({ label, icon, disabled, title, sections, wid
193
195
  }
194
196
  }
195
197
  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) => {
198
+ 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: iconOnly
199
+ ? className
200
+ : [
201
+ // `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
202
+ // whatever sits beside it in the toolbar off the edge of a 384px list column.
203
+ "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",
204
+ className,
205
+ ].join(" "), children: [icon, !iconOnly && _jsx("span", { className: "truncate", children: label }), !iconOnly && _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
206
  itemIndex += 1;
203
207
  const index = itemIndex;
204
208
  const role = item.role ?? "menuitem";
205
209
  return (_jsxs("button", { ref: (node) => {
206
210
  itemRefs.current[index] = node;
207
- }, type: "button", role: role, disabled: item.disabled, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
211
+ }, type: "button", role: role, disabled: item.disabled, title: item.title, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
208
212
  ? {}
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));
213
+ : { "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.icon && (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted", children: item.icon })), 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
214
  }), section.note && _jsx("p", { className: "px-3 py-1 text-xs leading-4 text-text-muted", children: section.note })] }, section.key))) }) }))] }));
211
215
  }
@@ -72,6 +72,11 @@ export interface MessageDetailPaneProps {
72
72
  * server-side 400 guard); Drafts is detected via `draftsFolderUid` (already passed by every caller
73
73
  * for the Outbox-cancel flow) rather than a new prop. */
74
74
  onArchived?: (updated: Message) => void;
75
+ /** Called with the server's updated copy (and the copy it replaces) after the card's More actions menu marked the message read or unread, or
76
+ * flagged or unflagged it - it stays where it is, so a caller patches it in place, as it does for `onLabelsChanged`. The menu's Delete, Report
77
+ * junk and Block move the message out of its folder and are reported through `onMoved`, which is what makes a thread's card leave it and the
78
+ * pane advance, exactly as after Archive. */
79
+ onChanged?: (updated: Message, previous?: Message) => void;
75
80
  /** Every label defined in this message's mailbox, for the label-assignment popover below — each
76
81
  * caller fetches its own mailbox's labels the same way it already resolves `draftsFolderUid`
77
82
  * (`listLabels()`). Absent/empty simply hides the Labels control - there's nothing to assign. */
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useRef, useState } from "react";
7
- import { HiOutlineArchiveBox, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlineFolderArrowDown, HiOutlineLockClosed, HiOutlineMoon, HiOutlineSun, } from "react-icons/hi2";
7
+ import { HiOutlineArchiveBox, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlineFolderArrowDown, HiOutlineInboxArrowDown, HiOutlineLockClosed, HiOutlineMoon, HiOutlineNoSymbol, HiOutlineSun, } from "react-icons/hi2";
8
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
9
  import { approveReceipt, archiveMessage, attachmentContentUrl, cancelScheduledSend, declineReceipt, getMailbox, getMessage, getMessageRawContent, moveMessage, recallMessage, setMessageLabels, } from "@rapidmx/react-shared/mail/mailApi.js";
10
10
  import { buildForwardQuote, buildReplyQuote, buildReplyRecipients, buildReplyThreading, forwardSubject, replySubject, } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
@@ -33,6 +33,10 @@ import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
33
33
  import { useShortcut } from "../../keyboard/useShortcut.js";
34
34
  import { useUnlockPrompt } from "../layout/UnlockPromptProvider.js";
35
35
  import { moveLocalEntity } from "../../search/localIndexRpcClient.js";
36
+ import { useNavigate } from "../../navigation/index.js";
37
+ import { notify } from "../../notifications/store.js";
38
+ import { notifyApiError } from "../../notifications/apiErrors.js";
39
+ import { useMailboxUpdateAccess } from "../../mail/useMailboxUpdateAccess.js";
36
40
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
37
41
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
38
42
  import LabelMenuButton from "./labelMenu.js";
@@ -41,6 +45,12 @@ import EncryptedBody from "./reading/EncryptedBody.js";
41
45
  import { displaySubject } from "./reading/EncryptedPreview.js";
42
46
  import MessageBody, { BodySkeleton } from "./reading/MessageBody.js";
43
47
  import { BODY_FONT_STYLE, CardShell, SenderAvatar, SubjectCard } from "./reading/MessageCard.js";
48
+ import MessageMoreMenu from "./reading/MessageMoreMenu.js";
49
+ import MessageSourceDialog from "./reading/MessageSourceDialog.js";
50
+ import { useMessageActions } from "./reading/useMessageActions.js";
51
+ import { saveAsEml } from "./reading/messageExport.js";
52
+ import { buildPrintDocument, printDocument } from "./reading/printMessage.js";
53
+ import { fetchBodyContent } from "./reading/bodyContent.js";
44
54
  import { useViewOriginal } from "./reading/viewOriginal.js";
45
55
  import { ROW_FOCUS_CLASS, UnreadLabel, dateClass, senderClass } from "./unreadStyle.js";
46
56
  /** Labels/styling for `specs/end-to-end_encryption.md`'s "Message Security Indicators" table - kept as
@@ -142,9 +152,11 @@ function downloadMimeAttachment(attachment) {
142
152
  * A plain `<button>` rather than react-shared's `Button`, whose padding and minimum width are sized for a
143
153
  * text label. Ordinary DOM order, so the keyboard reaches these in the order they are read.
144
154
  */
145
- function IconAction({ icon, label, onClick, disabled, busy, onPrefetch, shortcut, }) {
155
+ /** The look of the card's round icon buttons, shared with the "More actions" trigger. */
156
+ const ICON_BUTTON_CLASS = "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent";
157
+ function IconAction({ icon, label, onClick, disabled, reason, busy, onPrefetch, shortcut, }) {
146
158
  const env = useKeyEnvironment();
147
- return (_jsx("button", { type: "button", "aria-label": label, title: shortcut ? withHint(label, shortcut, env) : label, "aria-keyshortcuts": shortcut ? ariaKeyShortcuts(shortcut, env) : undefined, "aria-busy": busy || undefined, disabled: disabled, onClick: onClick, onPointerEnter: onPrefetch, onFocus: onPrefetch, className: "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: icon }));
159
+ return (_jsx("button", { type: "button", "aria-label": label, title: reason ?? (shortcut ? withHint(label, shortcut, env) : label), "aria-keyshortcuts": shortcut ? ariaKeyShortcuts(shortcut, env) : undefined, "aria-busy": busy || undefined, disabled: disabled, onClick: onClick, onPointerEnter: onPrefetch, onFocus: onPrefetch, className: ICON_BUTTON_CLASS, children: icon }));
148
160
  }
149
161
  function SecurityIndicator({ security }) {
150
162
  const { label, className } = SECURITY_INDICATOR[security.state];
@@ -170,9 +182,13 @@ const GENERIC_SIGNATURE_FAILURE_MESSAGE = "This message's digital signature coul
170
182
  /** Fallback when the raw MIME for an encrypted message can't even be fetched/evaluated - an encrypted
171
183
  * message must never be mislabeled "Unprotected" just because loading failed. */
172
184
  const ENCRYPTED_LOAD_ERROR = "Couldn't load this message's encrypted content.";
185
+ /** A subject without its \`Re:\`/\`Fwd:\` prefixes. */
186
+ function withoutSubjectPrefixes(subject) {
187
+ return (subject ?? "").replace(/^(\s*(re|fw|fwd|aw|sv)\s*:)+/i, "").trim();
188
+ }
173
189
  /** A subject without its \`Re:\`/\`Fwd:\` prefixes, lower-cased - what makes two messages of one thread the same subject. */
174
190
  function subjectCore(subject) {
175
- return (subject ?? "").replace(/^(\s*(re|fw|fwd|aw|sv)\s*:)+/i, "").trim().toLowerCase();
191
+ return withoutSubjectPrefixes(subject).toLowerCase();
176
192
  }
177
193
  /** Whether a message's subject says nothing its thread's subject doesn't. */
178
194
  function sameSubject(subject, threadSubject) {
@@ -209,7 +225,7 @@ export default function MessageDetailPane(props) {
209
225
  function needsRawSecurityEvaluation(message) {
210
226
  return !!message.encrypted || message.hasAttachments;
211
227
  }
212
- function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, labels, onLabelsChanged, onLabelCreated, inThread, threadSubject, threadHeader, footer, shortcuts, }) {
228
+ function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, onChanged, labels, onLabelsChanged, onLabelCreated, inThread, threadSubject, threadHeader, footer, shortcuts, }) {
213
229
  // A copy re-read from the server after an Outbox action was refused (409/403) or a send lease ran out - see
214
230
  // `reloadMessage()`. A prop copy newer by `version` wins; an equal-version reload is kept, since claiming a send
215
231
  // lease need not bump `version`.
@@ -678,6 +694,36 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
678
694
  useShortcut(SHORTCUTS.mail.forward, () => void (!preparingCompose && handleReplyOrForward("forward")), { enabled: keyboard });
679
695
  useShortcut(SHORTCUTS.mail.archive, () => void (!archiving && handleArchive()), { enabled: keyboard && archivable });
680
696
  useShortcut(SHORTCUTS.mail.move, () => setMovePrompt(true), { enabled: keyboard && movable });
697
+ // The card's Report junk button and "More actions" menu: they act on this message, in the mailbox it is in. Nothing of them applies to a message
698
+ // being written or sent (Drafts, Outbox), and Report junk and Block are not for mail the reader sent.
699
+ const folderType = folders?.find((folder) => folder.uid === message.folderUid)?.type;
700
+ const inJunk = folderType === "junk";
701
+ const inDeletedItems = folderType === "deleted_items";
702
+ const reportable = archivable && !isSentItems;
703
+ const writable = useMailboxUpdateAccess(readerMailbox);
704
+ const remember = (updated) => {
705
+ latestLabelsMessageRef.current = updated;
706
+ setLabelsMessage(updated);
707
+ };
708
+ // Every address the reader owns, in any of their mailboxes: a sender among them is not blocked.
709
+ const readerOwnAddresses = mailboxes.flatMap((mb) => [mb.primarySmtpAddress, ...(mb.aliasAddresses ?? [])]).filter((address) => !!address).map((address) => address.toLowerCase());
710
+ const messageActions = useMessageActions({
711
+ message,
712
+ ownAddresses: readerOwnAddresses,
713
+ newest: () => (message.version >= latestLabelsMessageRef.current.version ? message : latestLabelsMessageRef.current),
714
+ remember,
715
+ folders,
716
+ inJunk,
717
+ inDeletedItems,
718
+ trackMessageChange,
719
+ onMoved,
720
+ onChanged,
721
+ onFolderCreated,
722
+ });
723
+ const navigate = useNavigate();
724
+ const [sourceView, setSourceView] = useState(null);
725
+ // Which "View source" request the dialog is waiting for: one answering after the dialog was closed (or another was opened) is not shown.
726
+ const sourceRequestRef = useRef(0);
681
727
  // Only ever invoked from the pending-receipt banner below, which itself only renders once `message`
682
728
  // is loaded — same real invariant as every other handler above.
683
729
  async function handleReceipt(type, action) {
@@ -758,13 +804,107 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
758
804
  const senderRecipient = { displayName: senderName, address: senderAddress };
759
805
  const sendingStatus = (_jsxs(_Fragment, { children: [sendInProgress && (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Sending\u2026" })), isSentItems &&
760
806
  (message.recallRequestedAt ? (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Recall requested" })) : (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setConfirming(true), children: "Recall this message" }))), inOutbox && !sendInProgress && message.scheduledSendTime && (_jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("span", { className: "text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt", children: ["Scheduled for ", new Date(message.scheduledSendTime).toLocaleString()] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Cancel" })] })), inOutbox && !sendInProgress && !message.scheduledSendTime && draftsFolderUid && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Move to Drafts" }))] }));
807
+ // ---- The "More actions" menu's own rows (Print, View, Save as, Create rule); the rest are `messageActions`. ----
808
+ /** Prints this one message: its header lines and the body the pane shows (what was decrypted or verified here, else the server's sanitized
809
+ * body), in a frame of its own - see `printMessage.ts`. */
810
+ async function handlePrint() {
811
+ try {
812
+ const content = bodyContent ?? (await fetchBodyContent(message.uid, message.version));
813
+ const format = (list) => list.map((r) => formatMailAddress(r)).join(", ");
814
+ const printable = buildPrintDocument({
815
+ subject: shownSubject,
816
+ headers: [
817
+ { name: "From", value: senderLabel },
818
+ { name: "To", value: format(message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc")) },
819
+ { name: "Cc", value: format(message.recipients.filter((r) => r.type === "cc")) },
820
+ { name: "Date", value: new Date(message.receivedDate).toLocaleString() },
821
+ ],
822
+ content,
823
+ attachments,
824
+ inlineParts: innerAttachments,
825
+ });
826
+ if (printable === undefined) {
827
+ notify({ kind: "warning", title: "This message is too large to print here" });
828
+ return;
829
+ }
830
+ printDocument(printable);
831
+ }
832
+ catch (err) {
833
+ notifyApiError(err, "Couldn't print this message");
834
+ }
835
+ }
836
+ /** Opens the source (or just the headers) of the message as the server has it - for an encrypted message, its ciphertext. */
837
+ function openSource(mode) {
838
+ const request = ++sourceRequestRef.current;
839
+ setSourceView({ mode, source: { status: "loading" } });
840
+ getMessageRawContent(message.uid).then((raw) => {
841
+ if (sourceRequestRef.current === request) {
842
+ setSourceView({ mode, source: { status: "ready", raw } });
843
+ }
844
+ }, (err) => {
845
+ if (sourceRequestRef.current === request) {
846
+ setSourceView({ mode, source: { status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message's source." } });
847
+ }
848
+ });
849
+ }
850
+ function closeSource() {
851
+ sourceRequestRef.current++;
852
+ setSourceView(null);
853
+ }
854
+ async function handleSaveAsEml() {
855
+ try {
856
+ saveAsEml(await getMessageRawContent(message.uid), protectedSubject ?? displaySubject(message.subject));
857
+ }
858
+ catch (err) {
859
+ notifyApiError(err, "Couldn't save this message");
860
+ }
861
+ }
862
+ /** The rules page's "New mail filter", started from this message: its sender and (for a message the server can read) its subject. */
863
+ function handleCreateRule() {
864
+ const params = new URLSearchParams({ mailboxUid: message.mailboxUid, from: message.from.address });
865
+ const subject = message.encrypted ? "" : withoutSubjectPrefixes(message.subject);
866
+ if (subject) {
867
+ params.set("subject", subject);
868
+ }
869
+ navigate(`/settings/filters/new?${params.toString()}`);
870
+ }
871
+ const menuActions = {
872
+ replyAll: () => void handleReplyOrForward("replyAll"),
873
+ forward: () => void handleReplyOrForward("forward"),
874
+ deleteMessage: () => void messageActions.deleteMessage(),
875
+ toggleRead: () => void messageActions.toggleRead(),
876
+ toggleFlag: () => void messageActions.toggleFlag(),
877
+ reportJunk: () => void messageActions.reportJunk(),
878
+ reportPhishing: () => void messageActions.reportPhishing(),
879
+ notJunk: () => void messageActions.notJunk(),
880
+ notJunkAndTrust: () => void messageActions.notJunkAndTrust(),
881
+ blockSender: () => void messageActions.blockSender(),
882
+ neverBlockSender: () => void messageActions.neverBlockSender(),
883
+ print: () => void handlePrint(),
884
+ viewSource: () => openSource("source"),
885
+ viewDetails: () => openSource("headers"),
886
+ saveAsEml: () => void handleSaveAsEml(),
887
+ createRule: handleCreateRule,
888
+ };
889
+ // An encrypted message can be printed once it has been opened here: while it is still opening, or when it can't be, there is nothing to print.
890
+ const printReason = !message.encrypted
891
+ ? undefined
892
+ : security === null
893
+ ? "Still opening this message"
894
+ : security.decryptError === undefined
895
+ ? undefined
896
+ : lockedRef.current
897
+ ? "Unlock this message to print it"
898
+ : "This message can't be read, so it can't be printed";
899
+ // In Junk Email the card's button is the way back (Not junk), which only needs the right to change the message.
900
+ const reportReason = !writable ? "View-only mailbox" : undefined;
761
901
  const card = (_jsxs(CardShell, { unread: cardUnread, children: [_jsxs("div", { className: ["flex flex-wrap items-start gap-x-3 gap-y-1 px-4 pt-3 pb-2", cardUnread ? "bg-primary/[0.07]" : ""].join(" "), children: [_jsx(SenderAvatar, { from: senderRecipient }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [threadHeader ? (_jsx("h2", { className: "min-w-0 flex-1", style: BODY_FONT_STYLE, children: _jsxs("button", { type: "button", ref: threadHeader.buttonRef, onClick: threadHeader.onToggle, "aria-expanded": true, "aria-controls": threadHeader.bodyId, className: [
762
902
  "w-full text-left rounded-sm text-sm",
763
903
  ROW_FOCUS_CLASS,
764
904
  ].join(" "), children: [_jsx(UnreadLabel, { unread: cardUnread }), _jsx(MailAddress, { recipient: senderRecipient, className: senderClass(cardUnread) })] }) })) : (_jsxs("p", { className: "text-sm break-words min-w-0", children: ["From ", _jsx("span", { className: "font-semibold text-text", children: senderLabel })] })), security && _jsx(SecurityIndicator, { security: security })] }), senderNameCheck.misleading && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["The sender\u2019s name \u201C", senderName, "\u201D looks like an email address, but this message was sent from ", _jsx("span", { className: "font-medium", children: senderAddress }), ". Don\u2019t trust it based on the name."] })), _jsx(RecipientLine, { label: "To", recipients: message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc") }), _jsx(RecipientLine, { label: "Cc", recipients: message.recipients.filter((r) => r.type === "cc") }), _jsx(RecipientLine, { label: "Bcc", recipients: message.recipients.filter((r) => r.type === "bcc") })] }), _jsxs("div", { className: "ml-auto flex flex-wrap items-center justify-end gap-1", children: [_jsx("time", { dateTime: message.receivedDate, className: ["text-xs mr-1", dateClass(cardUnread)].join(" "), children: new Date(message.receivedDate).toLocaleString() }), adaptable && (_jsx("button", { type: "button", "aria-label": "View original", "aria-pressed": viewOriginal, title: viewOriginal ? "Follow the theme" : "View original", onClick: () => setViewOriginal(!viewOriginal), className: [
765
905
  "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt",
766
906
  viewOriginal ? "bg-primary/10" : "",
767
- ].join(" "), children: viewOriginal ? _jsx(HiOutlineMoon, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineSun, { size: 16, "aria-hidden": "true" }) })), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true" }), label: "Reply", shortcut: keyboard ? SHORTCUTS.mail.reply : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("reply") }), _jsx(IconAction, { icon: _jsxs("span", { className: "inline-flex items-center", "aria-hidden": "true", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16 }), _jsx(HiOutlineArrowUturnLeft, { size: 16, className: "-ml-2.5" })] }), label: "Reply All", shortcut: keyboard ? SHORTCUTS.mail.replyAll : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("replyAll") }), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnRight, { size: 16, "aria-hidden": "true" }), label: "Forward", shortcut: keyboard ? SHORTCUTS.mail.forward : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("forward") }), archivable && (_jsx(IconAction, { icon: _jsx(HiOutlineArchiveBox, { size: 16, "aria-hidden": "true" }), label: "Archive", shortcut: keyboard ? SHORTCUTS.mail.archive : undefined, busy: archiving, disabled: archiving, onClick: handleArchive })), movable && (_jsx(IconAction, { icon: _jsx(HiOutlineFolderArrowDown, { size: 16, "aria-hidden": "true" }), label: "Move to", shortcut: keyboard ? SHORTCUTS.mail.move : undefined, onClick: () => setMovePrompt(true) })), labels && labels.length > 0 && (_jsx(LabelMenuButton, { "aria-label": "Labels", label: "Labels", className: "border border-border py-1.5", labels: labels, mailboxUid: message.mailboxUid, onLabelCreated: onLabelCreated, applied: currentLabelsMessage.labelUids ?? [], onCommit: (labelUids) => void handleApplyLabels(labelUids), busy: savingLabels, note: "Ticked labels are applied to this message and unticked ones removed.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }))] })] }), (answered || forwarded) && (_jsx("p", { className: "px-4 py-1.5 text-xs text-text-muted bg-surface-alt border-y border-border", children: answered && forwarded ? "You replied to and forwarded this message." : answered ? "You replied to this message." : "You forwarded this message." })), _jsxs("div", { id: threadHeader?.bodyId, className: "px-4 pb-3 flex flex-col gap-2", children: [inThread && (subjectDiffers || !sameSubject(shownSubject, threadSubject)) && (_jsx("h3", { className: "text-sm font-semibold break-words", children: shownSubject })), _jsx("div", { className: "flex flex-wrap items-center gap-2 empty:hidden", children: sendingStatus }), inOutbox && message.scheduledSendError && _jsxs(Alert, { children: ["This message wasn\u2019t sent: ", message.scheduledSendError] }), cancelError && _jsx(Alert, { children: cancelError }), keyChangeNotice && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: keyChangeNotice })), security && UNVERIFIED_SIGNER_STATES.has(security.state) && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["Signed, but the signer isn\u2019t a trusted contact key, so the sender isn\u2019t verified.", security.signerEmails && security.signerEmails.length > 0 && _jsxs(_Fragment, { children: [" Certificate for ", security.signerEmails.join(", "), "."] }), security.signerFingerprint && (_jsxs(_Fragment, { children: [" ", "Fingerprint ", _jsx("span", { className: "font-mono text-xs break-all", children: security.signerFingerprint }), "."] }))] })), pendingConflict && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["This sender has a signing key change waiting for your review, so this signer can\u2019t be trusted from here.", " ", _jsx("a", { href: `/contacts/${encodeURIComponent(senderKeyState.contactUid)}`, className: "font-medium text-primary-dark hover:underline", children: "Review it in Contacts" })] })), trustableCertificate && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => {
907
+ ].join(" "), children: viewOriginal ? _jsx(HiOutlineMoon, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineSun, { size: 16, "aria-hidden": "true" }) })), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true" }), label: "Reply", shortcut: keyboard ? SHORTCUTS.mail.reply : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("reply") }), _jsx(IconAction, { icon: _jsxs("span", { className: "inline-flex items-center", "aria-hidden": "true", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16 }), _jsx(HiOutlineArrowUturnLeft, { size: 16, className: "-ml-2.5" })] }), label: "Reply All", shortcut: keyboard ? SHORTCUTS.mail.replyAll : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("replyAll") }), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnRight, { size: 16, "aria-hidden": "true" }), label: "Forward", shortcut: keyboard ? SHORTCUTS.mail.forward : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("forward") }), archivable && (_jsx(IconAction, { icon: _jsx(HiOutlineArchiveBox, { size: 16, "aria-hidden": "true" }), label: "Archive", shortcut: keyboard ? SHORTCUTS.mail.archive : undefined, busy: archiving, disabled: archiving, onClick: handleArchive })), movable && (_jsx(IconAction, { icon: _jsx(HiOutlineFolderArrowDown, { size: 16, "aria-hidden": "true" }), label: "Move to", shortcut: keyboard ? SHORTCUTS.mail.move : undefined, onClick: () => setMovePrompt(true) })), reportable && (_jsx(IconAction, { icon: inJunk ? _jsx(HiOutlineInboxArrowDown, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineNoSymbol, { size: 16, "aria-hidden": "true" }), label: inJunk ? "Not junk" : "Report junk", busy: messageActions.busy, disabled: messageActions.busy || reportReason !== undefined, reason: reportReason, onClick: () => void (inJunk ? messageActions.notJunk() : messageActions.reportJunk()) })), labels && labels.length > 0 && (_jsx(LabelMenuButton, { "aria-label": "Labels", label: "Labels", className: "border border-border py-1.5", labels: labels, mailboxUid: message.mailboxUid, onLabelCreated: onLabelCreated, applied: currentLabelsMessage.labelUids ?? [], onCommit: (labelUids) => void handleApplyLabels(labelUids), busy: savingLabels, note: "Ticked labels are applied to this message and unticked ones removed.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } })), archivable && (_jsx(MessageMoreMenu, { actions: menuActions, senderAddress: message.from.address, read: currentLabelsMessage.flags.read === true, flagged: currentLabelsMessage.flags.flagged === true, writable: writable, busy: messageActions.busy, inJunk: inJunk, inDeletedItems: inDeletedItems, sent: !!isSentItems, ownSender: readerOwnAddresses.includes(message.from.address.toLowerCase()), canTrustSender: writable, printReason: printReason, composing: preparingCompose, triggerClassName: ICON_BUTTON_CLASS }))] })] }), (answered || forwarded) && (_jsx("p", { className: "px-4 py-1.5 text-xs text-text-muted bg-surface-alt border-y border-border", children: answered && forwarded ? "You replied to and forwarded this message." : answered ? "You replied to this message." : "You forwarded this message." })), _jsxs("div", { id: threadHeader?.bodyId, className: "px-4 pb-3 flex flex-col gap-2", children: [inThread && (subjectDiffers || !sameSubject(shownSubject, threadSubject)) && (_jsx("h3", { className: "text-sm font-semibold break-words", children: shownSubject })), _jsx("div", { className: "flex flex-wrap items-center gap-2 empty:hidden", children: sendingStatus }), inOutbox && message.scheduledSendError && _jsxs(Alert, { children: ["This message wasn\u2019t sent: ", message.scheduledSendError] }), cancelError && _jsx(Alert, { children: cancelError }), keyChangeNotice && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: keyChangeNotice })), security && UNVERIFIED_SIGNER_STATES.has(security.state) && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["Signed, but the signer isn\u2019t a trusted contact key, so the sender isn\u2019t verified.", security.signerEmails && security.signerEmails.length > 0 && _jsxs(_Fragment, { children: [" Certificate for ", security.signerEmails.join(", "), "."] }), security.signerFingerprint && (_jsxs(_Fragment, { children: [" ", "Fingerprint ", _jsx("span", { className: "font-mono text-xs break-all", children: security.signerFingerprint }), "."] }))] })), pendingConflict && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["This sender has a signing key change waiting for your review, so this signer can\u2019t be trusted from here.", " ", _jsx("a", { href: `/contacts/${encodeURIComponent(senderKeyState.contactUid)}`, className: "font-medium text-primary-dark hover:underline", children: "Review it in Contacts" })] })), trustableCertificate && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => {
768
908
  setTrustError(null);
769
909
  setTrustConfirmOpen(true);
770
910
  }, children: "Trust this signer" }) })), subjectDiffers && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["The subject shown above is the one the sender signed. It differs from the subject this message was delivered with (\u201C", message.subject, "\u201D), which may have been changed on the way, e.g. by a mailing list."] })), security?.headerTamperDetected && (_jsx(Alert, { children: "This message's visible From/To/Cc/Date/Subject don't match what the sender actually signed or encrypted \u2014 an intermediary may have altered them after sending. Treat the fields shown above with caution." })), security?.state === "verified_at_first_open" && (_jsx("p", { role: "status", className: `py-2 px-3 rounded-sm text-sm text-text ${security.laterCompromised ? "bg-warning/15" : "bg-surface-alt"}`, children: verifiedAtFirstOpenMessage(security) })), keyChanged && (_jsxs("section", { "aria-label": "Signing key changed", className: "py-3 px-3 rounded-sm text-sm bg-warning/15 text-text flex flex-col gap-2", children: [_jsx("h2", { className: "font-semibold", children: "This sender\u2019s signing key changed" }), _jsxs("p", { children: ["The signature on this message is valid and its certificate names ", senderAddress, ", but it was made with a different key than the one you trust for this sender", keyChanged.state === "verified_at_first_open" ? "." : _jsx(_Fragment, { children: ", so it isn\u2019t verified." })] }), _jsx(KeyChangeReview, { mailboxUid: message.mailboxUid, address: senderAddress, useType: "sign", ownerName: "the sender", current: pinnedSignerKey && { fingerprint: pinnedSignerKey.fingerprint, since: senderKeyState.pinnedSince }, proposed: {
@@ -782,5 +922,5 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
782
922
  .map((type) => (_jsxs("div", { className: "flex flex-wrap items-center gap-2 py-2 px-3 rounded-sm bg-surface-alt text-sm", children: [_jsxs("span", { children: [senderLabel, " requested a ", type, " receipt for this message."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: receiptBusy === type, disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "approve"), children: "Send receipt" }), _jsx(Button, { type: "button", variant: "text", disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "decline"), children: "Decline" })] }, type))), receiptError && _jsx(Alert, { children: receiptError }), mayHoldInvite && _jsx(InviteCard, { messageUid: message.uid, headingLevel: inThread ? 3 : 2 }), security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: "These attachments come from a message whose signature couldn\u2019t be verified. Open them only if you trust the sender." })), innerAttachments
783
923
  ? innerAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: innerAttachments.map((attachment, index) => (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => downloadMimeAttachment(attachment), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: attachment.filename ?? `Unnamed ${attachment.contentType} attachment` }) }, `${index}:${attachment.filename ?? ""}`))) }))
784
924
  : listedAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: listedAttachments.map((attachment) => (_jsx("li", { children: _jsxs("a", { href: attachmentContentUrl(attachment.uid), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: [attachment.filename, " (", formatBytes(attachment.sizeBytes), ")"] }) }, attachment.uid))) })), _jsx("div", { ref: bodyRegionRef, tabIndex: -1, className: "outline-none", children: message.encrypted && security === null ? (_jsx(BodySkeleton, {})) : security?.decryptError !== undefined ? (_jsx(EncryptedBody, { locked: lockedRef.current, reason: security.decryptError, unlocking: unlocking, onUnlock: () => void handleUnlockToView() })) : (_jsx(MessageBody, { messageUid: message.uid, messageVersion: message.version, title: bodyTitle, content: bodyContent, attachments: attachments, inlineParts: innerAttachments, original: viewOriginal, onAdaptable: setAdaptable })) })] }), showFooter && (_jsxs("div", { className: "flex flex-wrap gap-2 px-4 py-3 border-t border-border print:hidden", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("reply"), children: "Reply" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply all to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("replyAll"), children: "Reply All" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Forward this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("forward"), children: "Forward" })] }))] }));
785
- return (_jsxs("div", { className: inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full", children: [inThread ? (card) : (_jsxs(_Fragment, { children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" })), _jsx(SubjectCard, { subject: shownSubject }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: card })] })), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
925
+ return (_jsxs("div", { className: inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full", children: [inThread ? (card) : (_jsxs(_Fragment, { children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" })), _jsx(SubjectCard, { subject: shownSubject }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: card })] })), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), messageActions.dialog, _jsx(MessageSourceDialog, { open: sourceView !== null, onClose: closeSource, mode: sourceView?.mode ?? "source", source: sourceView?.source ?? { status: "loading" }, encrypted: !!message.encrypted }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
786
926
  }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ export interface PermanentDeleteDialogProps {
3
+ open: boolean;
4
+ title: string;
5
+ /** The question, ending in what cannot be undone. */
6
+ message: string;
7
+ /** The destructive button's own words ("Delete permanently"). */
8
+ confirmLabel: string;
9
+ /** The delete is on the wire: the dialog stays up, and can be neither confirmed again nor dismissed. */
10
+ busy: boolean;
11
+ onConfirm: () => void;
12
+ onCancel: () => void;
13
+ }
14
+ /**
15
+ * The confirmation in front of every permanent delete. A `Modal`, so Escape and the Cancel button dismiss it and Tab stays inside it. Focus
16
+ * starts on the dialog itself, never on the destructive button, so pressing Enter (or the Delete key again) on a dialog the reader did not
17
+ * mean to open keeps their mail. It remembers nothing: every permanent delete asks.
18
+ */
19
+ export default function PermanentDeleteDialog({ open, title, message, confirmLabel, busy, onConfirm, onCancel }: PermanentDeleteDialogProps): React.JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
3
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
4
+ /**
5
+ * The confirmation in front of every permanent delete. A `Modal`, so Escape and the Cancel button dismiss it and Tab stays inside it. Focus
6
+ * starts on the dialog itself, never on the destructive button, so pressing Enter (or the Delete key again) on a dialog the reader did not
7
+ * mean to open keeps their mail. It remembers nothing: every permanent delete asks.
8
+ */
9
+ export default function PermanentDeleteDialog({ open, title, message, confirmLabel, busy, onConfirm, onCancel }) {
10
+ return (_jsxs(Modal, { open: open, onClose: () => !busy && onCancel(), title: title, children: [_jsx("p", { className: "text-sm mb-5", children: message }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: onCancel, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: busy, disabled: busy, onClick: onConfirm, children: confirmLabel })] })] }));
11
+ }
@@ -3,7 +3,7 @@ import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js"
3
3
  import { AppShellProps } from "../../layout/AppShell.js";
4
4
  import { LiveUpdates } from "../../../mail/useMailLiveUpdates.js";
5
5
  import { MAILBOX_LIST_LIMIT } from "../../../mail/useMailConnection.js";
6
- import { CountTracker } from "../../../mail/folderCounts.js";
6
+ import { CountTracker, FolderCount } from "../../../mail/folderCounts.js";
7
7
  export type MailShellProps = Omit<AppShellProps, "active">;
8
8
  /** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
9
9
  * mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
@@ -59,6 +59,13 @@ export interface MailShellContextValue {
59
59
  * context value, which is only ever read outside a real shell.
60
60
  */
61
61
  trackMessageChange: (previous: Message, next: Message | null) => CountTracker;
62
+ /**
63
+ * Reads every mailbox's real folder counts back and shows them (after `delayMs`, 600 ms by default) - for a change too big to track message by
64
+ * message, such as emptying a folder. A no-op on the default context value, which is only ever read outside a real shell.
65
+ */
66
+ refreshFolderCounts: (delayMs?: number) => void;
67
+ /** A folder's counts as the sidebar shows them: what the shell has worked out or been told since it loaded, else what the folder was listed with. */
68
+ folderCountOf: (folder: Folder) => FolderCount;
62
69
  /**
63
70
  * The phone layout's header row beside the folders button, for the page to put its search box in (`createPortal()`): the shell
64
71
  * owns the row, the page owns the search state. `null` until the row has rendered, and on the default context value.
@@ -71,12 +78,10 @@ export { MAILBOX_LIST_LIMIT };
71
78
  /**
72
79
  * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
73
80
  * aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
74
- * impersonation banner — see that component). There is no client-side router in this framework (see
75
- * `ReactRoute`'s file-convention resolver) — the selected mailbox/folder (or aggregate pseudo-folder) live
76
- * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read once on mount (never during
77
- * the initial render itself, matching every other query-param reader in this codebase — e.g.
78
- * `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
79
- * match).
81
+ * impersonation banner — see that component). The selected mailbox/folder (or aggregate pseudo-folder) live
82
+ * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read from the router's location
83
+ * (`@rapidrest/react`), which is the request's on the server and while hydrating and follows a shallow
84
+ * navigation (a folder link) without remounting the page.
80
85
  *
81
86
  * Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
82
87
  * anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
@@ -85,8 +90,7 @@ export { MAILBOX_LIST_LIMIT };
85
90
  * of folders" requirement.
86
91
  *
87
92
  * An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
88
- * the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. Nothing is selected until the address
89
- * has been read. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
93
+ * the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
90
94
  * primary mailbox.
91
95
  */
92
96
  export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;