@rapidmx/web-client 0.15.1 → 0.17.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.
Files changed (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -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
+ }
@@ -10,6 +10,7 @@ import { assembleDraft, attachmentContentUrl, createDraft, deleteMessage, getMai
10
10
  import { listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
11
11
  import { buildComposeBodyHtml } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
12
12
  import { peekMailboxWritability, useMailboxWritability } from "../writableMailboxes.js";
13
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
13
14
  import { resolveRecipientEncryption } from "@rapidmx/react-shared/crypto/composeSecurity.js";
14
15
  import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
15
16
  import { findActivePublicKey, getEncryptionPolicy, lookupKeys } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
@@ -219,11 +220,13 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
219
220
  if (cancelled) {
220
221
  return;
221
222
  }
222
- setMailboxes(result);
223
+ // Listed with the caller's own (primary) mailbox first, and defaulted to below.
224
+ const ordered = orderMailboxes(result, userUid);
225
+ setMailboxes(ordered);
223
226
  // Never default to a mailbox already known to be view-only (see the effect below for one
224
227
  // that turns out to be).
225
- const candidates = result.filter((mb) => peekMailboxWritability(mb, userUid, trusted) !== false);
226
- setFromMailboxUid((current) => current ?? (candidates.find((mb) => mb.ownerUserUid === userUid) ?? candidates[0])?.uid);
228
+ const candidates = ordered.filter((mb) => peekMailboxWritability(mb, userUid, trusted) !== false);
229
+ setFromMailboxUid((current) => current ?? primaryMailboxUid(candidates, userUid));
227
230
  })
228
231
  .catch((err) => {
229
232
  // Only fatal when there's no mailbox to fall back on - a session that already names one just
@@ -481,7 +484,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
481
484
  if (!fromIsViewOnly || hasUploads) {
482
485
  return;
483
486
  }
484
- const ordered = [...mailboxes.filter((mb) => mb.ownerUserUid === userUid), ...mailboxes.filter((mb) => mb.ownerUserUid !== userUid)];
487
+ const ordered = orderMailboxes(mailboxes, userUid);
485
488
  const alternative = ordered.find((mb) => writability[mb.uid] === true) ?? ordered.find((mb) => writability[mb.uid] === undefined);
486
489
  if (alternative) {
487
490
  handleFromChange(alternative.uid);
@@ -685,6 +688,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
685
688
  encryptRequested: current.encryptRequested,
686
689
  forcePlaintext,
687
690
  saved: saveInFlightRef.current,
691
+ threading: session.threading,
688
692
  };
689
693
  if (request.to.length === 0) {
690
694
  setSendError("At least one recipient is required.");
@@ -22,6 +22,26 @@ export interface RecipientInputProps {
22
22
  disabled?: boolean;
23
23
  /** Focuses the input when it mounts - a new message starts in To. */
24
24
  autoFocus?: boolean;
25
+ /** Called whenever the field's entries change: every committed entry (chips - a picked suggestion, a finished or
26
+ * pasted one, or a typed one committed by Enter or leaving the field) and the text still being typed (`""` after a
27
+ * commit). Lets a field that keeps its entries as something other than text (attendees) read them without parsing
28
+ * `value` and guessing which part of it is still being typed. */
29
+ onEdit?: (chips: string[], pending: string) => void;
30
+ /** Text being typed already when the input mounts (the last recipient of `value`), so a field that is remounted
31
+ * (a popover growing into a form) keeps it in the input instead of turning it into a chip. */
32
+ initialPending?: string;
33
+ /** Chips this returns true for are not drawn (they stay in the value, and out of the suggestions): a field that
34
+ * lists some entries itself. Backspace in an empty input removes the last chip that is drawn. */
35
+ hideChip?: (chip: string) => boolean;
36
+ placeholder?: string;
37
+ /** The input's accessible name, for a field without a `<label htmlFor>`. */
38
+ ariaLabel?: string;
39
+ /** The chip list's accessible name; `${label} recipients` by default. */
40
+ listLabel?: string;
41
+ /** Classes for the field's outer element, after its own (a border, padding). */
42
+ className?: string;
43
+ /** The suggestions' `z-index`, for a field inside an overlay that stacks above the default's (`z-[60]`). */
44
+ dropdownZIndex?: number;
25
45
  }
26
46
  /**
27
47
  * A To/Cc/Bcc field: committed recipients as removable chips, and a WAI-ARIA combobox for the one being typed that
@@ -33,4 +53,4 @@ export interface RecipientInputProps {
33
53
  * least two characters, and a newer query aborts an older request. Arrow keys move through them, Enter or Tab picks
34
54
  * one (as `Name <address>`), Escape closes them, and Backspace in an empty input removes the last chip.
35
55
  */
36
- export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions, debounceMs, disabled, autoFocus, }: RecipientInputProps): React.JSX.Element;
56
+ export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions, debounceMs, disabled, autoFocus, onEdit, initialPending, hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }: RecipientInputProps): React.JSX.Element;
@@ -41,8 +41,8 @@ function dropdownPosition(anchor) {
41
41
  * least two characters, and a newer query aborts an older request. Arrow keys move through them, Enter or Tab picks
42
42
  * one (as `Name <address>`), Escape closes them, and Backspace in an empty input removes the last chip.
43
43
  */
44
- export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, }) {
45
- const [pending, setPending] = useState("");
44
+ export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, onEdit, initialPending = "", hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }) {
45
+ const [pending, setPending] = useState(initialPending);
46
46
  const [suggestions, setSuggestions] = useState([]);
47
47
  const [open, setOpen] = useState(false);
48
48
  const [activeIndex, setActiveIndex] = useState(0);
@@ -58,6 +58,8 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
58
58
  const chips = pendingActive ? tokens.slice(0, -1) : tokens;
59
59
  const inputValue = pendingActive ? pending : "";
60
60
  const query = pendingActive ? pendingText : "";
61
+ const drawn = chips.map((chip, index) => ({ chip, index })).filter(({ chip }) => !hideChip?.(chip));
62
+ const lastDrawn = drawn.length > 0 ? drawn[drawn.length - 1].index : -1;
61
63
  const listboxId = `${id}-suggestions`;
62
64
  const optionId = (index) => `${id}-suggestion-${index}`;
63
65
  useEffect(() => {
@@ -113,6 +115,7 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
113
115
  });
114
116
  function emit(nextChips, nextPending) {
115
117
  onChange([...nextChips, nextPending.trim()].filter((part) => part.length > 0).join(", "));
118
+ onEdit?.(nextChips, nextPending.trim());
116
119
  }
117
120
  function handleInputChange(e) {
118
121
  const { finished, rest } = splitTypedRecipients(e.target.value);
@@ -122,17 +125,22 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
122
125
  emit([...chips, ...finished], nextPending);
123
126
  }
124
127
  function commitPending() {
125
- const next = [...chips, pendingText].filter((part) => part.length > 0).join(", ");
128
+ const committed = [...chips, pendingText].filter((part) => part.length > 0);
126
129
  setPending("");
127
130
  setOpen(false);
128
- return next;
131
+ if (pendingText) {
132
+ onEdit?.(committed, "");
133
+ }
134
+ return committed.join(", ");
129
135
  }
130
136
  function select(suggestion) {
131
- const next = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })].join(", ");
137
+ const committed = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })];
138
+ const next = committed.join(", ");
132
139
  setPending("");
133
140
  setOpen(false);
134
141
  setSuggestions([]);
135
142
  onChange(next);
143
+ onEdit?.(committed, "");
136
144
  onCommit?.(next);
137
145
  }
138
146
  function removeChip(index) {
@@ -181,9 +189,9 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
181
189
  }
182
190
  return;
183
191
  case "Backspace":
184
- if (inputValue === "" && chips.length > 0) {
192
+ if (inputValue === "" && lastDrawn >= 0) {
185
193
  e.preventDefault();
186
- emit(chips.slice(0, -1), "");
194
+ emit(chips.filter((_, i) => i !== lastDrawn), "");
187
195
  }
188
196
  return;
189
197
  }
@@ -192,12 +200,12 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
192
200
  const next = commitPending();
193
201
  onBlur?.(next);
194
202
  }
195
- return (_jsxs("div", { ref: anchorRef, className: "flex-1 min-w-0 flex flex-wrap items-center gap-1", children: [chips.length > 0 && (_jsx("ul", { role: "list", "aria-label": `${label} recipients`, className: "contents", children: chips.map((chip, index) => {
203
+ return (_jsxs("div", { ref: anchorRef, className: `flex-1 min-w-0 flex flex-wrap items-center gap-1${className ? ` ${className}` : ""}`, children: [drawn.length > 0 && (_jsx("ul", { role: "list", "aria-label": listLabel ?? `${label} recipients`, className: "contents", children: drawn.map(({ chip, index }) => {
196
204
  const recipient = parseRecipient(chip);
197
205
  const valid = isValidRecipientAddress(recipient.address);
198
206
  const name = recipient.displayName || recipient.address;
199
207
  return (_jsxs("li", { title: chip, className: `flex items-center gap-1 max-w-full min-w-0 rounded-pill border pl-2 pr-0.5 py-px text-xs ${valid ? "bg-surface-alt border-border text-text" : "bg-danger-bg border-danger text-danger"}`, children: [_jsx("span", { className: "truncate", children: name }), !valid && _jsx("span", { className: "sr-only", children: " (not a valid email address)" }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Remove ${recipient.address}`, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: () => removeChip(index), className: "shrink-0 w-4 h-4 flex items-center justify-center rounded-pill text-text-muted hover:bg-border hover:text-text", children: _jsx(HiOutlineXMark, { size: 10 }) })] }, `${index}-${chip}`));
200
- }) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
208
+ }) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-label": ariaLabel, placeholder: placeholder, "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
201
209
  position &&
202
- createPortal(_jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${label} suggestions`, onMouseDown: (e) => e.preventDefault(), style: { position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight }, className: "z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1", children: visible.map((suggestion, index) => (_jsxs("li", { id: optionId(index), role: "option", "aria-selected": index === active, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActiveIndex(index), onClick: () => select(suggestion), className: `flex items-center gap-2 px-3 py-1.5 cursor-pointer ${index === active ? "bg-primary/10" : ""}`, children: [_jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate text-sm text-text", children: suggestion.displayName || suggestion.address }), suggestion.displayName && _jsx("span", { className: "block truncate text-xs text-text-muted", children: suggestion.address })] }), _jsx("span", { className: "shrink-0 text-[11px] leading-4 px-1.5 rounded-pill bg-surface-alt text-text-muted", children: KIND_LABELS[suggestion.kind] ?? "Directory" })] }, `${index}-${suggestion.address}`))) }), document.body)] }));
210
+ createPortal(_jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${label} suggestions`, onMouseDown: (e) => e.preventDefault(), style: { position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight, zIndex: dropdownZIndex }, className: "z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1", children: visible.map((suggestion, index) => (_jsxs("li", { id: optionId(index), role: "option", "aria-selected": index === active, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActiveIndex(index), onClick: () => select(suggestion), className: `flex items-center gap-2 px-3 py-1.5 cursor-pointer ${index === active ? "bg-primary/10" : ""}`, children: [_jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate text-sm text-text", children: suggestion.displayName || suggestion.address }), suggestion.displayName && _jsx("span", { className: "block truncate text-xs text-text-muted", children: suggestion.address })] }), _jsx("span", { className: "shrink-0 text-[11px] leading-4 px-1.5 rounded-pill bg-surface-alt text-text-muted", children: KIND_LABELS[suggestion.kind] ?? "Directory" })] }, `${index}-${suggestion.address}`))) }), document.body)] }));
203
211
  }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import { FolderBadge } from "../../../mail/folderCounts.js";
3
+ /** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
4
+ export default function FolderBadgeChip({ badge }: {
5
+ badge: FolderBadge;
6
+ }): React.JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { badgeLabel } from "../../../mail/folderCounts.js";
3
+ /** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
4
+ export default function FolderBadgeChip({ badge }) {
5
+ return (_jsxs("span", { className: [
6
+ "text-xs rounded-pill py-0.5 px-1.5",
7
+ badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
8
+ ].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
9
+ }
@@ -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,17 +78,19 @@ 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
83
88
  * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
84
89
  * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
85
90
  * of folders" requirement.
91
+ *
92
+ * An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
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
94
+ * primary mailbox.
86
95
  */
87
96
  export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;