@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
@@ -11,8 +11,10 @@ import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
+ import { useLocation } from "@rapidrest/react/client";
15
+ import { useNavigate } from "../../../navigation/index.js";
15
16
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
18
  import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
17
19
  /** Every settings section with its own sidebar entry. */
18
20
  export const SETTINGS_SECTIONS = [
@@ -20,6 +22,7 @@ export const SETTINGS_SECTIONS = [
20
22
  { id: "appearance", href: "/settings/appearance", label: "Appearance" },
21
23
  { id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
22
24
  { id: "filters", href: "/settings/filters", label: "Mail Filters" },
25
+ { id: "blocked-senders", href: "/settings/blocked-senders", label: "Blocked & Safe Senders" },
23
26
  { id: "signatures", href: "/settings/signatures", label: "Signatures" },
24
27
  { id: "labels", href: "/settings/labels", label: "Labels" },
25
28
  { id: "read-receipts", href: "/settings/read-receipts", label: "Read Receipts" },
@@ -57,7 +60,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
57
60
  const [drawerOpen, setDrawerOpen] = useState(false);
58
61
  // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
59
62
  // effect, not during render, so the server render and the hydrating render agree.
60
- const search = useLocationSearch();
63
+ const { search } = useLocation();
61
64
  const navigate = useNavigate();
62
65
  useEffect(() => {
63
66
  setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
@@ -73,7 +76,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
73
76
  }
74
77
  listMailboxes({ limit: 100 })
75
78
  .then((result) => {
76
- setMailboxes(result);
79
+ setMailboxes(orderMailboxes(result, userUid));
77
80
  setStatus("ready");
78
81
  })
79
82
  .catch((err) => {
@@ -82,7 +85,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
82
85
  });
83
86
  }, [userUid]);
84
87
  const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
85
- mailboxes[0]?.uid;
88
+ primaryMailboxUid(mailboxes, userUid);
86
89
  const contextValue = useMemo(() => ({ mailboxUid, mailboxes }), [mailboxUid, mailboxes]);
87
90
  // A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
88
91
  // TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
@@ -110,8 +113,8 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
110
113
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
111
114
  // `inner` (this whole branch) is only ever computed once `status === "ready"`
112
115
  // and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
113
- // `<MailboxProvisioning />` above instead (and TypeScript's own narrowing of
114
- // this `const` already reflects that, no `!` needed) — so no fallback is
116
+ // `<MailboxProvisioning />` above instead (which is what the `!` below records,
117
+ // `primaryMailboxUid()` being honest that it may find none) — so no fallback is
115
118
  // needed here, matching this plan's established "dead guard the UI
116
119
  // structurally can't trigger" removal precedent (e.g.
117
120
  // `DomainDetailContent.handleVerify`).
@@ -119,7 +122,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
119
122
  "block px-2.5 py-1.5 rounded-sm text-sm",
120
123
  section.id === active ? "bg-primary/10 text-primary-dark font-medium" : "text-text hover:bg-surface-alt",
121
124
  ].join(" "), children: section.label }, section.id))) })] }));
122
- inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
125
+ inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
123
126
  }
124
127
  return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
125
128
  }
@@ -79,5 +79,5 @@ export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSel
79
79
  // `Drawer` (see `ContactsSidebar` for the identical pattern and the duplicate-id reasoning behind
80
80
  // the `idPrefix`).
81
81
  const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "My Day", count: myDayCount, active: active.type === "myDay", onClick: () => handleSelect({ type: "myDay" }) }), _jsx(NavItem, { label: "Important", count: importantCount, active: active.type === "important", onClick: () => handleSelect({ type: "important" }) }), _jsx(NavItem, { label: "Planned", count: plannedCount, active: active.type === "planned", onClick: () => handleSelect({ type: "planned" }) }), _jsx(NavItem, { label: "Assigned to me", count: assignedToMeCount, active: active.type === "assignedToMe", onClick: () => handleSelect({ type: "assignedToMe" }) }), _jsx(NavItem, { label: "Flagged email", active: active.type === "flagged", onClick: () => handleSelect({ type: "flagged" }) })] }), _jsx(NavItem, { label: "Tasks", count: tasks.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] })] }));
82
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
82
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
83
83
  }
@@ -11,8 +11,10 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
+ import { useLocation } from "@rapidrest/react/client";
15
+ import { useNavigate } from "../../../navigation/index.js";
15
16
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
18
  const TasksShellContext = createContext({ mailboxes: [] });
17
19
  /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
18
20
  export function useTasksShell() {
@@ -35,7 +37,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
35
37
  const [drawerOpen, setDrawerOpen] = useState(false);
36
38
  // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
37
39
  // effect, not during render, so the server render and the hydrating render agree.
38
- const search = useLocationSearch();
40
+ const { search } = useLocation();
39
41
  const navigate = useNavigate();
40
42
  useEffect(() => {
41
43
  setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
@@ -51,7 +53,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
51
53
  }
52
54
  listMailboxes({ limit: 100 })
53
55
  .then((result) => {
54
- setMailboxes(result);
56
+ setMailboxes(orderMailboxes(result, userUid));
55
57
  setStatus("ready");
56
58
  })
57
59
  .catch((err) => {
@@ -60,7 +62,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
60
62
  });
61
63
  }, [userUid]);
62
64
  const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
63
- mailboxes[0]?.uid;
65
+ primaryMailboxUid(mailboxes, userUid);
64
66
  useEffect(() => {
65
67
  if (!mailboxUid) {
66
68
  setFolders([]);
@@ -98,7 +100,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
98
100
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
99
101
  // With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
100
102
  const hasSidebar = mailboxes.length > 1 || !!folderError;
101
- inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
103
+ inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
102
104
  }
103
105
  return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
104
106
  }
@@ -4,7 +4,7 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { accountUrlOf } from "../auth/accountUrl.js";
6
6
  import { APP_HREFS, SETTINGS_HREF, isSettingsPath } from "../navigation/appHrefs.js";
7
- import { useNavigate } from "../navigation/routerContext.js";
7
+ import { useNavigate } from "../navigation/index.js";
8
8
  import { SHORTCUTS } from "./keymap.js";
9
9
  import { useShortcut } from "./useShortcut.js";
10
10
  /**
@@ -46,6 +46,16 @@ export declare function badgeLabel(badge: FolderBadge): string;
46
46
  export declare function inboxUnreadTotal(mailboxFolders: {
47
47
  folders: Folder[];
48
48
  }[], counts: Record<string, FolderCount>): number;
49
+ /**
50
+ * The folder types whose unread count is left out of a collapsed sidebar section's heading badge: Drafts and Outbox (their badges
51
+ * count messages held, not unread mail), and Sent Items, Deleted Items and Junk Email (an unread count there is noise, not mail
52
+ * to notice). It matches the folders `badgeFor()` shows no unread badge for; every other type - Inbox, Archive, the user's own
53
+ * folders - counts, so mail that a rule filed out of the Inbox is not overlooked in a hidden section. Change it here to change that.
54
+ */
55
+ export declare const UNREAD_BADGE_EXCLUDED_FOLDER_TYPES: readonly FolderType[];
56
+ /** The unread messages in the `folders` that carry an unread count (all but `UNREAD_BADGE_EXCLUDED_FOLDER_TYPES`) - what a collapsed
57
+ * sidebar section shows on its heading, so new mail in a hidden section is still noticed. */
58
+ export declare function unreadBadgeTotal(folders: readonly Folder[], counts: Record<string, FolderCount>): number;
49
59
  /** How long after the last change of this page's own has finished a server-published count is trusted again: one published
50
60
  * about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
51
61
  export declare const COUNT_QUIET_MS = 1500;
@@ -51,6 +51,18 @@ export function badgeLabel(badge) {
51
51
  export function inboxUnreadTotal(mailboxFolders, counts) {
52
52
  return mailboxFolders.reduce((total, entry) => total + entry.folders.filter((folder) => folder.type === "inbox").reduce((sum, folder) => sum + countOfFolder(folder, counts).unread, 0), 0);
53
53
  }
54
+ /**
55
+ * The folder types whose unread count is left out of a collapsed sidebar section's heading badge: Drafts and Outbox (their badges
56
+ * count messages held, not unread mail), and Sent Items, Deleted Items and Junk Email (an unread count there is noise, not mail
57
+ * to notice). It matches the folders `badgeFor()` shows no unread badge for; every other type - Inbox, Archive, the user's own
58
+ * folders - counts, so mail that a rule filed out of the Inbox is not overlooked in a hidden section. Change it here to change that.
59
+ */
60
+ export const UNREAD_BADGE_EXCLUDED_FOLDER_TYPES = ["drafts", "outbox", "sent_items", "deleted_items", "junk"];
61
+ /** The unread messages in the `folders` that carry an unread count (all but `UNREAD_BADGE_EXCLUDED_FOLDER_TYPES`) - what a collapsed
62
+ * sidebar section shows on its heading, so new mail in a hidden section is still noticed. */
63
+ export function unreadBadgeTotal(folders, counts) {
64
+ return folders.reduce((total, folder) => (UNREAD_BADGE_EXCLUDED_FOLDER_TYPES.includes(folder.type) ? total : total + countOfFolder(folder, counts).unread), 0);
65
+ }
54
66
  /** How long after the last change of this page's own has finished a server-published count is trusted again: one published
55
67
  * about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
56
68
  export const COUNT_QUIET_MS = 1500;
@@ -0,0 +1,13 @@
1
+ import { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /** Forgets every remembered folder - for the tests. */
3
+ export declare function clearResolvedFolders(): void;
4
+ /**
5
+ * A mailbox's own folder of `type` (Junk Email, Deleted Items, Inbox) - the reading pane's counterpart of the mail page's bulk
6
+ * `resolveFolderOfType()`, so Report junk and Delete on one message go where the selection bar's go.
7
+ *
8
+ * Folders belong to a mailbox: only those of `mailboxUid` in `known` count (a message opened from a shared mailbox, or from a search
9
+ * over several, is filed in ITS mailbox's Junk, never the open one's). The server makes every well-known folder itself, so one the tree
10
+ * lacks is asked for - the tree may only be out of date - and used, never created twice; only a server that really has none is asked to
11
+ * make it. `onFound` is told about a folder the tree did not have, so the sidebar picks it up.
12
+ */
13
+ export declare function resolveFolderOfType(mailboxUid: string, type: Folder["type"], name: string, known?: Folder[], onFound?: (folder: Folder) => void): Promise<string>;
@@ -0,0 +1,40 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { createFolder, listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ /** The folders this page load has had to look up or make for a mailbox, so a second Report junk does not ask again. */
7
+ const resolved = new Map();
8
+ /** Forgets every remembered folder - for the tests. */
9
+ export function clearResolvedFolders() {
10
+ resolved.clear();
11
+ }
12
+ /**
13
+ * A mailbox's own folder of `type` (Junk Email, Deleted Items, Inbox) - the reading pane's counterpart of the mail page's bulk
14
+ * `resolveFolderOfType()`, so Report junk and Delete on one message go where the selection bar's go.
15
+ *
16
+ * Folders belong to a mailbox: only those of `mailboxUid` in `known` count (a message opened from a shared mailbox, or from a search
17
+ * over several, is filed in ITS mailbox's Junk, never the open one's). The server makes every well-known folder itself, so one the tree
18
+ * lacks is asked for - the tree may only be out of date - and used, never created twice; only a server that really has none is asked to
19
+ * make it. `onFound` is told about a folder the tree did not have, so the sidebar picks it up.
20
+ */
21
+ export async function resolveFolderOfType(mailboxUid, type, name, known = [], onFound) {
22
+ const inTree = known.find((folder) => folder.mailboxUid === mailboxUid && folder.type === type);
23
+ if (inTree) {
24
+ return inTree.uid;
25
+ }
26
+ const key = `${mailboxUid}:${type}`;
27
+ const remembered = resolved.get(key);
28
+ if (remembered) {
29
+ return remembered;
30
+ }
31
+ const listed = (await listFolders(mailboxUid)).find((folder) => folder.type === type);
32
+ if (listed) {
33
+ onFound?.(listed);
34
+ resolved.set(key, listed.uid);
35
+ return listed.uid;
36
+ }
37
+ const created = await createFolder({ mailboxUid, name, type });
38
+ resolved.set(key, created.uid);
39
+ return created.uid;
40
+ }
@@ -0,0 +1,80 @@
1
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { MailAddressLike } from "@rapidmx/react-shared/mail/mailAddress.js";
3
+ import type { NotificationAction } from "../../notifications/store.js";
4
+ import type { SendRequest } from "./sendJob.js";
5
+ /**
6
+ * A reply (or forward) this tab has sent, as the open conversation shows it until the server's own copy is in the thread.
7
+ *
8
+ * This is a *view* of the background send that is already there, not a second state machine: `startSend()` (`sendJob.ts`) opens an entry, and the
9
+ * places that already report how a send went close it - the client-side failure (`notifyFailure()`), the server's own outcome (`handleSendEvent()`
10
+ * in `sendOutcomes.ts`: `send-succeeded`, `send-failed`) and a synchronous send. The Outbox indicator, the pop-ups and this entry therefore agree,
11
+ * because they are all told by the same code at the same moment.
12
+ *
13
+ * Why the message has to be shown from here for a while: a message the server accepted is in Outbox, which has no `conversationId` yet - it is
14
+ * resolved when the message is relayed and filed in Sent Items (`ScheduledSendJob`), keeping the same `uid`. Until then no fetch of the
15
+ * conversation can return it, so the thread pane draws it from what was composed.
16
+ *
17
+ * `sending` covers everything up to the server's answer (assembling, the request, the queue in Outbox); `failed` is a send that did not go and is
18
+ * the sender's to resolve (`actions` are the very ones the pop-up offers: Retry, Open draft ...); `sent` is a message the server has relayed and
19
+ * that is waiting for the thread to be read again to find its Sent Items copy.
20
+ */
21
+ export type OutgoingState = "sending" | "failed" | "sent";
22
+ export interface OutgoingReply {
23
+ /** The draft's uid - which is also the uid of the message in Outbox and of its Sent Items copy, and so what the real message replaces this by. */
24
+ uid: string;
25
+ mailboxUid: string;
26
+ /** What the message replies to (`Message.inReplyTo` / `references`): how the pane knows which conversation it belongs to. */
27
+ inReplyTo?: string;
28
+ references: string[];
29
+ sender: MailAddressLike;
30
+ to: MailAddressLike[];
31
+ cc: MailAddressLike[];
32
+ bcc: MailAddressLike[];
33
+ subject: string;
34
+ /** The composed body, as the editor wrote it. */
35
+ html: string;
36
+ startedAt: number;
37
+ state: OutgoingState;
38
+ /** `failed`: why. */
39
+ failure?: string;
40
+ /** `failed`: what the sender can do about it. */
41
+ actions: NotificationAction[];
42
+ /** `sent`: how many times the thread was read without finding the message - see `settleOutgoing()`. */
43
+ misses: number;
44
+ }
45
+ /** A sent message that a read of its conversation has not found this many times is not part of that conversation (the server filed it elsewhere). */
46
+ export declare const MAX_SETTLE_MISSES = 2;
47
+ /**
48
+ * Starts showing `request`'s message in the conversation it replies to - or does nothing for a message that replies to nothing (a new message
49
+ * starts a conversation of its own, it is never part of an open one) and for a send-later message (it is not sent yet). Called again for the same
50
+ * message - a Retry - it starts over as `sending`.
51
+ *
52
+ * The thread is `request.threading` (the compose session's), else what the draft itself records: a draft opened again from "Open draft" is a
53
+ * session with no threading of its own, but the server kept it on the message.
54
+ */
55
+ export declare function trackOutgoing(request: SendRequest): void;
56
+ /** The send did not go: `failure` is why, `actions` what can be done (Retry, Open draft ...). */
57
+ export declare function failOutgoing(uid: string, failure: string, actions: NotificationAction[]): void;
58
+ /** A failed send is being tried again. */
59
+ export declare function markOutgoingSending(uid: string): void;
60
+ /** The server has relayed the message: its Sent Items copy is what the conversation shows next. */
61
+ export declare function markOutgoingSent(uid: string): void;
62
+ /** Stops showing a message - it went back to Drafts to be edited. */
63
+ export declare function forgetOutgoing(uid: string): void;
64
+ /** Whether `reply` continues the conversation `messages` are: it answers one of them, or its `References` chain runs through one. */
65
+ export declare function belongsToThread(reply: OutgoingReply, messages: readonly Pick<Message, "messageId">[]): boolean;
66
+ /** The uids of the messages sent from here whose real copy is in `messages`, the thread as it was just read. */
67
+ export declare function adoptedOutgoing(mailboxUid: string, messages: readonly Message[]): string[];
68
+ /**
69
+ * Called with a mailbox's conversation as it was just read, `messages` being the whole thread: forgets the messages whose real copy is now in it
70
+ * (`adoptedOutgoing()`), and gives up on a *sent* message that belongs to this thread but that the read did not find `MAX_SETTLE_MISSES` times in a
71
+ * row - the server filed it in a conversation of its own, and it is not to be shown here. A message of another conversation is left alone.
72
+ *
73
+ * A pane draws the messages it has read *before* it calls this, so that there is never a moment with neither the pending card nor the real one.
74
+ */
75
+ export declare function settleOutgoing(mailboxUid: string, messages: readonly Message[]): void;
76
+ export declare function getOutgoingReplies(): readonly OutgoingReply[];
77
+ /** The messages this tab has sent that a conversation may still need to show, oldest first. */
78
+ export declare function useOutgoingReplies(): readonly OutgoingReply[];
79
+ /** Forgets everything - for a test. */
80
+ export declare function resetOutgoingReplies(): void;
@@ -0,0 +1,131 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useSyncExternalStore } from "react";
6
+ /** The most entries kept; the oldest go first. Every entry is a message this tab sent, so this is generous. */
7
+ const MAX_TRACKED = 25;
8
+ /** A sent message that a read of its conversation has not found this many times is not part of that conversation (the server filed it elsewhere). */
9
+ export const MAX_SETTLE_MISSES = 2;
10
+ const NO_REPLIES = [];
11
+ let replies = [];
12
+ let snapshot = replies;
13
+ const listeners = new Set();
14
+ function commit(next) {
15
+ replies = next;
16
+ snapshot = next;
17
+ for (const listener of [...listeners]) {
18
+ listener();
19
+ }
20
+ }
21
+ function update(uid, patch) {
22
+ if (replies.some((reply) => reply.uid === uid)) {
23
+ commit(replies.map((reply) => (reply.uid === uid ? { ...reply, ...patch } : reply)));
24
+ }
25
+ }
26
+ /**
27
+ * Starts showing `request`'s message in the conversation it replies to - or does nothing for a message that replies to nothing (a new message
28
+ * starts a conversation of its own, it is never part of an open one) and for a send-later message (it is not sent yet). Called again for the same
29
+ * message - a Retry - it starts over as `sending`.
30
+ *
31
+ * The thread is `request.threading` (the compose session's), else what the draft itself records: a draft opened again from "Open draft" is a
32
+ * session with no threading of its own, but the server kept it on the message.
33
+ */
34
+ export function trackOutgoing(request) {
35
+ const source = request.threading ?? request.draft;
36
+ const references = source.references ?? [];
37
+ if (request.scheduledSendTime || (!source.inReplyTo && references.length === 0)) {
38
+ return;
39
+ }
40
+ const { mailbox } = request;
41
+ const reply = {
42
+ uid: request.draft.uid,
43
+ mailboxUid: request.mailboxUid,
44
+ inReplyTo: source.inReplyTo,
45
+ references,
46
+ // The mailbox is only unknown when the window's own load of it had not succeeded: "Me" is what a sender is called then.
47
+ sender: mailbox ? { address: mailbox.primarySmtpAddress, displayName: mailbox.displayName } : { address: "Me" },
48
+ to: request.to,
49
+ cc: request.cc,
50
+ bcc: request.bcc,
51
+ subject: request.subject,
52
+ html: request.html,
53
+ startedAt: Date.now(),
54
+ state: "sending",
55
+ actions: [],
56
+ misses: 0,
57
+ };
58
+ commit([...replies.filter((existing) => existing.uid !== reply.uid), reply].slice(-MAX_TRACKED));
59
+ }
60
+ /** The send did not go: `failure` is why, `actions` what can be done (Retry, Open draft ...). */
61
+ export function failOutgoing(uid, failure, actions) {
62
+ update(uid, { state: "failed", failure, actions });
63
+ }
64
+ /** A failed send is being tried again. */
65
+ export function markOutgoingSending(uid) {
66
+ update(uid, { state: "sending", failure: undefined, actions: [] });
67
+ }
68
+ /** The server has relayed the message: its Sent Items copy is what the conversation shows next. */
69
+ export function markOutgoingSent(uid) {
70
+ update(uid, { state: "sent" });
71
+ }
72
+ /** Stops showing a message - it went back to Drafts to be edited. */
73
+ export function forgetOutgoing(uid) {
74
+ if (replies.some((reply) => reply.uid === uid)) {
75
+ commit(replies.filter((reply) => reply.uid !== uid));
76
+ }
77
+ }
78
+ /** Whether `reply` continues the conversation `messages` are: it answers one of them, or its `References` chain runs through one. */
79
+ export function belongsToThread(reply, messages) {
80
+ return messages.some((message) => !!message.messageId && (message.messageId === reply.inReplyTo || reply.references.includes(message.messageId)));
81
+ }
82
+ /** The uids of the messages sent from here whose real copy is in `messages`, the thread as it was just read. */
83
+ export function adoptedOutgoing(mailboxUid, messages) {
84
+ return replies.filter((reply) => reply.mailboxUid === mailboxUid && messages.some((message) => message.uid === reply.uid)).map((reply) => reply.uid);
85
+ }
86
+ /**
87
+ * Called with a mailbox's conversation as it was just read, `messages` being the whole thread: forgets the messages whose real copy is now in it
88
+ * (`adoptedOutgoing()`), and gives up on a *sent* message that belongs to this thread but that the read did not find `MAX_SETTLE_MISSES` times in a
89
+ * row - the server filed it in a conversation of its own, and it is not to be shown here. A message of another conversation is left alone.
90
+ *
91
+ * A pane draws the messages it has read *before* it calls this, so that there is never a moment with neither the pending card nor the real one.
92
+ */
93
+ export function settleOutgoing(mailboxUid, messages) {
94
+ const adopted = new Set(adoptedOutgoing(mailboxUid, messages));
95
+ const next = [];
96
+ let changed = adopted.size > 0;
97
+ for (const reply of replies) {
98
+ if (adopted.has(reply.uid)) {
99
+ continue;
100
+ }
101
+ if (reply.mailboxUid === mailboxUid && reply.state === "sent" && belongsToThread(reply, messages)) {
102
+ changed = true;
103
+ if (reply.misses + 1 < MAX_SETTLE_MISSES) {
104
+ next.push({ ...reply, misses: reply.misses + 1 });
105
+ }
106
+ }
107
+ else {
108
+ next.push(reply);
109
+ }
110
+ }
111
+ if (changed) {
112
+ commit(next);
113
+ }
114
+ }
115
+ export function getOutgoingReplies() {
116
+ return snapshot;
117
+ }
118
+ function subscribe(listener) {
119
+ listeners.add(listener);
120
+ return () => {
121
+ listeners.delete(listener);
122
+ };
123
+ }
124
+ /** The messages this tab has sent that a conversation may still need to show, oldest first. */
125
+ export function useOutgoingReplies() {
126
+ return useSyncExternalStore(subscribe, getOutgoingReplies, () => NO_REPLIES);
127
+ }
128
+ /** Forgets everything - for a test. */
129
+ export function resetOutgoingReplies() {
130
+ commit([]);
131
+ }
@@ -1,4 +1,4 @@
1
- import { Attachment, ComposeRecipientInput, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
1
+ import { Attachment, ComposeRecipientInput, DraftThreading, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
2
  import { RecipientEncryptionStatus } from "@rapidmx/react-shared/crypto/composeSecurity.js";
3
3
  import { EncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
4
4
  import { SendDecisionInput } from "./sendDecision.js";
@@ -34,6 +34,10 @@ export interface SendRequest {
34
34
  forcePlaintext: boolean;
35
35
  /** Settles when the window's own autosaves have landed, so none can overwrite the assembly below. */
36
36
  saved?: Promise<unknown>;
37
+ /** The thread this message continues (a reply or forward), as the compose session was opened with it - what lets the conversation on screen
38
+ * show the message as soon as it is sent (see `outgoingReplies.ts`). Absent for a new message, and for a draft opened again, which the draft's own
39
+ * `inReplyTo`/`references` stand in for. */
40
+ threading?: DraftThreading;
37
41
  }
38
42
  /** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
39
43
  export declare const CONTEXT_GRACE_MS = 4000;
@@ -13,6 +13,7 @@ import { policyCanAutoEncrypt } from "../../components/mail/compose/encryptionRe
13
13
  import { dismiss, notify } from "../../notifications/store.js";
14
14
  import { INLINE_IMAGE_PATTERN, decideSend } from "./sendDecision.js";
15
15
  import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingSends.js";
16
+ import { failOutgoing, forgetOutgoing, markOutgoingSending, markOutgoingSent, trackOutgoing } from "./outgoingReplies.js";
16
17
  import { openComposeFromOutside, requestUnlockFromOutside } from "./composeBridge.js";
17
18
  import { sendState } from "./sendState.js";
18
19
  /** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
@@ -209,7 +210,7 @@ export function describeMessage(subject, recipients) {
209
210
  }
210
211
  /** Re-opens the compose window around a request's message, with everything as it was typed. */
211
212
  export function openDraftFromRequest(request) {
212
- return openComposeFromOutside({
213
+ const opened = openComposeFromOutside({
213
214
  mailboxUid: request.mailboxUid,
214
215
  resume: {
215
216
  draft: request.draft,
@@ -225,6 +226,11 @@ export function openDraftFromRequest(request) {
225
226
  encryptRequested: request.encryptRequested,
226
227
  },
227
228
  });
229
+ // Being edited again, it is no longer a message on its way in the conversation.
230
+ if (opened) {
231
+ forgetOutgoing(request.draft.uid);
232
+ }
233
+ return opened;
228
234
  }
229
235
  function failureNotificationId(draftUid) {
230
236
  return `send-failed:${draftUid}`;
@@ -257,6 +263,8 @@ function notifyFailure(request, failure) {
257
263
  details: failure.details,
258
264
  actions,
259
265
  });
266
+ // The conversation on screen shows the same failure under the message, with the same ways out.
267
+ failOutgoing(uid, failure.message, actions);
260
268
  }
261
269
  const SENT_BURST_MS = 6000;
262
270
  /** The short "Message sent" confirmation - several sent close together are one pop-up counting up, never a stack of them. */
@@ -280,6 +288,8 @@ export function startSend(request) {
280
288
  }
281
289
  // The failure of an earlier attempt is resolved by trying again.
282
290
  dismiss(failureNotificationId(uid));
291
+ // A reply or forward is on screen in its conversation from this moment, before any of the work below.
292
+ trackOutgoing(request);
283
293
  const tracker = sendState.countTracker?.(request.mailboxUid);
284
294
  void attempt(request).then((outcome) => {
285
295
  finishPendingSend(uid);
@@ -294,6 +304,8 @@ export function startSend(request) {
294
304
  sendState.queuedListener?.(request.mailboxUid);
295
305
  }
296
306
  else if (outcome.kind === "sent") {
307
+ // A server with no queue relayed it before answering: the Sent Items copy exists already.
308
+ markOutgoingSent(uid);
297
309
  notifySent();
298
310
  }
299
311
  else {
@@ -312,6 +324,7 @@ export function startSend(request) {
312
324
  export async function retrySend(request) {
313
325
  if (await alreadyAccepted(request)) {
314
326
  dismiss(failureNotificationId(request.draft.uid));
327
+ markOutgoingSending(request.draft.uid);
315
328
  notify({ id: `already-sent:${request.draft.uid}`, kind: "info", title: "This message is already on its way", timeoutMs: 5000 });
316
329
  return;
317
330
  }
@@ -9,6 +9,7 @@ import { loadOriginalMessage } from "../../components/mail/compose/quotedBody.js
9
9
  import { notifyApiError } from "../../notifications/apiErrors.js";
10
10
  import { dismiss, notify } from "../../notifications/store.js";
11
11
  import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingSends.js";
12
+ import { failOutgoing, forgetOutgoing, markOutgoingSending, markOutgoingSent } from "./outgoingReplies.js";
12
13
  import { openComposeFromOutside } from "./composeBridge.js";
13
14
  import { describeMessage, forgetRetainedRequest, notifySent, openDraftFromRequest, retainedRequest } from "./sendJob.js";
14
15
  /**
@@ -39,6 +40,8 @@ function escapeHtml(text) {
39
40
  async function openFailedDraft(event) {
40
41
  try {
41
42
  const moved = await moveBackToDrafts(event.uid, event.mailboxUid ?? "");
43
+ // Back in Drafts it is no longer a message on its way in its conversation.
44
+ forgetOutgoing(event.uid);
42
45
  const request = retainedRequest(event.uid);
43
46
  if (request) {
44
47
  openDraftFromRequest({ ...request, draft: moved });
@@ -84,6 +87,7 @@ async function retryFailed(event) {
84
87
  return;
85
88
  }
86
89
  dismiss(failedId(event.uid));
90
+ markOutgoingSending(event.uid);
87
91
  try {
88
92
  setPendingStage(event.uid, "queuing");
89
93
  const result = await queueMessageSend(event.uid);
@@ -103,6 +107,8 @@ export function handleSendEvent(event) {
103
107
  if (event.action === "send-succeeded") {
104
108
  dismiss(retryId(event.uid));
105
109
  dismiss(failedId(event.uid));
110
+ // The conversation on screen swaps the message it drew for the Sent Items copy (a message this tab did not send has nothing drawn).
111
+ markOutgoingSent(event.uid);
106
112
  // The server reports every message it relays, scheduled ones from long ago included: only a message this tab queued in the background is
107
113
  // worth a confirmation (several are one pop-up counting up). Anything else - a send-later message going out, another tab's send - is quiet.
108
114
  if (retainedRequest(event.uid)) {
@@ -127,15 +133,18 @@ export function handleSendEvent(event) {
127
133
  return;
128
134
  }
129
135
  dismiss(retryId(event.uid));
136
+ const actions = [
137
+ { label: "Retry", onClick: () => void retryFailed(event) },
138
+ { label: "Open draft", onClick: () => void openFailedDraft(event) },
139
+ ];
130
140
  notify({
131
141
  id: failedId(event.uid),
132
142
  kind: "error",
133
143
  title: "This message wasn't sent",
134
144
  message: `${what}: ${failure.message}`,
135
145
  details: failure.lines,
136
- actions: [
137
- { label: "Retry", onClick: () => void retryFailed(event) },
138
- { label: "Open draft", onClick: () => void openFailedDraft(event) },
139
- ],
146
+ actions,
140
147
  });
148
+ // The conversation on screen shows the same failure under the message, with the same ways out.
149
+ failOutgoing(event.uid, failure.message, actions);
141
150
  }