@rapidmx/web-client 0.10.0 → 0.12.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 (324) hide show
  1. package/README.md +183 -2
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/mailboxes/new/index.tsx +3 -3
  7. package/apps/admin/quarantine/index.tsx +2 -1
  8. package/apps/admin/signing-certificates/index.tsx +343 -0
  9. package/apps/escrow/_layout.tsx +8 -2
  10. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  11. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  12. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  13. package/apps/shared/appearance/ColorField.tsx +107 -0
  14. package/apps/shared/appearance/appearanceCache.ts +106 -0
  15. package/apps/shared/appearance/appearanceContext.ts +77 -0
  16. package/apps/shared/appearance/bootScript.ts +35 -0
  17. package/apps/shared/appearance/color.ts +169 -0
  18. package/apps/shared/appearance/photo.ts +88 -0
  19. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  20. package/apps/shared/appearance/theme.ts +355 -0
  21. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +4 -4
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +60 -14
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +65 -16
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  28. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  29. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +98 -32
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  38. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  39. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  40. package/apps/shared/components/layout/UserMenu.tsx +75 -6
  41. package/apps/shared/components/mail/ConversationList.tsx +4 -2
  42. package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
  43. package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
  44. package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
  45. package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
  46. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  47. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
  49. package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
  50. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  51. package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
  52. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  53. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  54. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  55. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  56. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  57. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  58. package/apps/shared/components/mail/reading/color.ts +245 -0
  59. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  60. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  61. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  62. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  63. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  64. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  65. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  66. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  67. package/apps/shared/components/sharing/PrincipalListField.tsx +82 -0
  68. package/apps/shared/components/sharing/PrincipalPicker.tsx +76 -0
  69. package/apps/shared/components/sharing/PrincipalResolver.tsx +164 -0
  70. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  71. package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
  72. package/apps/shared/mail/folderCounts.ts +9 -3
  73. package/apps/shared/mail/folderTree.ts +143 -0
  74. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  75. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  76. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  77. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  78. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  79. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  81. package/apps/shared/mail/outbox/sendState.ts +35 -0
  82. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  83. package/apps/shared/mail/useMailConnection.ts +81 -28
  84. package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
  85. package/apps/shared/mail/useNewMailNotifications.ts +47 -32
  86. package/apps/shared/navigation/AppRouter.tsx +2 -2
  87. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  88. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  89. package/apps/shared/notifications/apiErrors.ts +98 -0
  90. package/apps/shared/notifications/headerOffset.ts +35 -0
  91. package/apps/shared/notifications/pushStatus.ts +56 -0
  92. package/apps/shared/notifications/store.ts +549 -0
  93. package/apps/shared/notifications/systemErrors.ts +63 -0
  94. package/apps/shared/notifications/useNotifications.ts +41 -0
  95. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  96. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  97. package/apps/shared/signing/enrollmentView.ts +251 -0
  98. package/apps/shared/signing/signingInfo.ts +58 -0
  99. package/apps/shared/signing/useNow.ts +19 -0
  100. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  101. package/apps/shared/styles/app.css +80 -0
  102. package/apps/www/_layout.tsx +8 -2
  103. package/apps/www/_routes.ts +1 -0
  104. package/apps/www/calendar/index.tsx +9 -14
  105. package/apps/www/contacts/[uid].tsx +5 -8
  106. package/apps/www/contacts/index.tsx +587 -584
  107. package/apps/www/index.tsx +38 -27
  108. package/apps/www/messages/[uid].tsx +3 -1
  109. package/apps/www/settings/appearance/index.tsx +21 -0
  110. package/apps/www/settings/auto-reply/index.tsx +3 -7
  111. package/apps/www/settings/encryption/index.tsx +151 -116
  112. package/apps/www/settings/filters/[uid].tsx +3 -1
  113. package/apps/www/settings/filters/new/index.tsx +3 -1
  114. package/apps/www/settings/privacy/index.tsx +3 -8
  115. package/apps/www/settings/read-receipts/index.tsx +3 -7
  116. package/apps/www/settings/sharing/index.tsx +44 -65
  117. package/apps/www/settings/signatures/[uid].tsx +3 -1
  118. package/apps/www/settings/signatures/new/index.tsx +3 -2
  119. package/apps/www/tasks/index.tsx +11 -16
  120. package/dist/apps/admin/_layout.d.ts +5 -1
  121. package/dist/apps/admin/_layout.js +3 -2
  122. package/dist/apps/admin/index.js +3 -2
  123. package/dist/apps/admin/ingest-queue/index.js +2 -1
  124. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  125. package/dist/apps/admin/mailboxes/new/index.js +1 -1
  126. package/dist/apps/admin/quarantine/index.js +2 -1
  127. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  128. package/dist/apps/admin/signing-certificates/index.js +161 -0
  129. package/dist/apps/escrow/_layout.d.ts +5 -1
  130. package/dist/apps/escrow/_layout.js +3 -2
  131. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  132. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  133. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  134. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  135. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  136. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  137. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  138. package/dist/apps/shared/appearance/ColorField.js +40 -0
  139. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  140. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  141. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  142. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  143. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  144. package/dist/apps/shared/appearance/bootScript.js +33 -0
  145. package/dist/apps/shared/appearance/color.d.ts +65 -0
  146. package/dist/apps/shared/appearance/color.js +132 -0
  147. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  148. package/dist/apps/shared/appearance/photo.js +79 -0
  149. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  150. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  151. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  152. package/dist/apps/shared/appearance/theme.js +251 -0
  153. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +3 -2
  154. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  155. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  156. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  157. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  158. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  159. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  160. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +25 -5
  161. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +18 -7
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  163. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  164. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  165. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  166. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  167. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  168. package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
  169. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
  170. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  171. package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
  172. package/dist/apps/shared/components/layout/AppShell.js +59 -13
  173. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  174. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  175. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  176. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  177. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  178. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  179. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  180. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  181. package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
  182. package/dist/apps/shared/components/layout/UserMenu.js +41 -7
  183. package/dist/apps/shared/components/mail/ConversationList.js +3 -2
  184. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
  185. package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
  186. package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
  187. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
  188. package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
  189. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
  190. package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
  191. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  192. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  193. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  194. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  195. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
  196. package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
  197. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
  198. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
  199. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  200. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  201. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
  202. package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
  203. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  204. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  205. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  206. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  207. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  208. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  209. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  210. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  211. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  212. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  213. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  214. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  215. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  216. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  217. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  218. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  219. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  220. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  221. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  222. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  223. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  224. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  225. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  226. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  227. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  228. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  229. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  230. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  231. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  232. package/dist/apps/shared/components/sharing/PrincipalListField.d.ts +24 -0
  233. package/dist/apps/shared/components/sharing/PrincipalListField.js +23 -0
  234. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +28 -0
  235. package/dist/apps/shared/components/sharing/PrincipalPicker.js +24 -0
  236. package/dist/apps/shared/components/sharing/PrincipalResolver.d.ts +44 -0
  237. package/dist/apps/shared/components/sharing/PrincipalResolver.js +92 -0
  238. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  239. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
  240. package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
  241. package/dist/apps/shared/mail/folderCounts.js +9 -3
  242. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  243. package/dist/apps/shared/mail/folderTree.js +116 -0
  244. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  245. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  246. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  247. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  248. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  249. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  250. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  251. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  252. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  253. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  254. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  255. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  256. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  257. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  258. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  259. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  260. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  261. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  262. package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
  263. package/dist/apps/shared/mail/useMailConnection.js +64 -22
  264. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
  265. package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
  266. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
  267. package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
  268. package/dist/apps/shared/navigation/AppRouter.js +2 -2
  269. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  270. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  271. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  272. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  273. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  274. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  275. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  276. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  277. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  278. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  279. package/dist/apps/shared/notifications/store.d.ts +136 -0
  280. package/dist/apps/shared/notifications/store.js +416 -0
  281. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  282. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  283. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  284. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  285. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  286. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  287. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  288. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  289. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  290. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  291. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  292. package/dist/apps/shared/signing/signingInfo.js +48 -0
  293. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  294. package/dist/apps/shared/signing/useNow.js +18 -0
  295. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  296. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  297. package/dist/apps/shared/styles/app.css +80 -0
  298. package/dist/apps/www/_layout.d.ts +5 -1
  299. package/dist/apps/www/_layout.js +3 -2
  300. package/dist/apps/www/_routes.js +1 -0
  301. package/dist/apps/www/calendar/index.js +3 -4
  302. package/dist/apps/www/contacts/[uid].js +5 -7
  303. package/dist/apps/www/contacts/index.js +14 -21
  304. package/dist/apps/www/index.js +37 -19
  305. package/dist/apps/www/messages/[uid].js +3 -1
  306. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  307. package/dist/apps/www/settings/appearance/index.js +13 -0
  308. package/dist/apps/www/settings/auto-reply/index.js +3 -6
  309. package/dist/apps/www/settings/encryption/index.js +103 -95
  310. package/dist/apps/www/settings/filters/[uid].js +3 -1
  311. package/dist/apps/www/settings/filters/new/index.js +3 -1
  312. package/dist/apps/www/settings/privacy/index.js +5 -8
  313. package/dist/apps/www/settings/read-receipts/index.js +3 -6
  314. package/dist/apps/www/settings/sharing/index.js +19 -33
  315. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  316. package/dist/apps/www/settings/signatures/new/index.js +3 -2
  317. package/dist/apps/www/tasks/index.js +8 -14
  318. package/package.json +2 -2
  319. package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
  320. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  321. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
  322. package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
  323. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  324. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -1,18 +1,13 @@
1
1
  import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import { FOLDER_ORDER, MAIL_FOLDER_TYPES } from "./folderTree.js";
2
3
  import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
3
4
  import { LiveUpdates } from "./useMailLiveUpdates.js";
4
5
  import type { FolderCounts } from "./folderCounts.js";
5
6
  import { NewMailNotifications } from "./useNewMailNotifications.js";
7
+ import { OutboxFolderStatus } from "./outbox/outboxState.js";
6
8
  /** Page size of the one `listMailboxes()` call. */
7
9
  export declare const MAILBOX_LIST_LIMIT = 100;
8
- /** Well-known folders sort first in the sidebar, in Gmail/Outlook's conventional order; anything else (incl. `user`) sorts after, alphabetically. */
9
- export declare const FOLDER_ORDER: string[];
10
- /**
11
- * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
12
- * Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
13
- * to just the mail ones itself, or those other apps' folders leak into its sidebar.
14
- */
15
- export declare const MAIL_FOLDER_TYPES: Set<string>;
10
+ export { FOLDER_ORDER, MAIL_FOLDER_TYPES };
16
11
  export type MailConnectionStatus = "checking" | "error" | "ready";
17
12
  /** Everything the mail apps share about the user's mailboxes, and the one live connection that keeps it current. */
18
13
  export interface MailConnection {
@@ -27,12 +22,18 @@ export interface MailConnection {
27
22
  foldersLoading: boolean;
28
23
  /** Files a folder that was just created (by this client or another) under its mailbox, so the sidebar and every folder picker show it. */
29
24
  onFolderCreated: (folder: Folder) => void;
25
+ /** Says which folders some message the page has met lives in (a list's rows, a conversation's folders): one the sidebar does not know is asked for
26
+ * - its mailbox's folders are listed again, once, debounced - and files it. Each unknown uid is asked about once, so a folder Mail never lists
27
+ * (another app's) cannot make the page ask again and again. */
28
+ noteFolderUids: (folderUids: Iterable<string>) => void;
30
29
  /** Bumped whenever new mail may have arrived - see `useMailLiveUpdates()`. */
31
30
  live: LiveUpdates;
32
31
  /** The folder badges' numbers and how to change them. */
33
32
  folderCounts: FolderCounts;
34
33
  /** The new-mail pop-ups and desktop notifications, and what feeds them. */
35
34
  notifications: NewMailNotifications;
35
+ /** What the messages in each Outbox folder are doing, by folder uid (the Outbox indicator's state) - see `useOutboxStatus()`. */
36
+ outbox: Record<string, OutboxFolderStatus>;
36
37
  }
37
38
  /** What the persistent app frame (`AppChrome`) offers to the Mail shell so that it doesn't open a second connection. `null` outside a frame. */
38
39
  export declare const MailConnectionContext: import("react").Context<MailConnection | null>;
@@ -2,21 +2,21 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import { createContext, useCallback, useEffect, useState } from "react";
5
+ import { createContext, useCallback, useEffect, useRef, useState } from "react";
6
6
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
7
  import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import { FOLDER_ORDER, MAIL_FOLDER_TYPES, isMailFolder, knownFolderUids, reconcileFolders, removeFolder, upsertFolder } from "./folderTree.js";
8
9
  import { useMailLiveUpdates } from "./useMailLiveUpdates.js";
9
10
  import { useNewMailNotifications } from "./useNewMailNotifications.js";
11
+ import { usePushConnectionNotice } from "../notifications/pushStatus.js";
12
+ import { registerOutboxCounter } from "./outbox/sendJob.js";
13
+ import { handleSendEvent } from "./outbox/sendOutcomes.js";
14
+ import { useOutboxStatus } from "./outbox/useOutboxStatus.js";
10
15
  /** Page size of the one `listMailboxes()` call. */
11
16
  export const MAILBOX_LIST_LIMIT = 100;
12
- /** Well-known folders sort first in the sidebar, in Gmail/Outlook's conventional order; anything else (incl. `user`) sorts after, alphabetically. */
13
- export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "junk", "archive", "deleted_items"];
14
- /**
15
- * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
16
- * Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
17
- * to just the mail ones itself, or those other apps' folders leak into its sidebar.
18
- */
19
- export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
17
+ // The sidebar's order and the folders Mail lists live in `folderTree.ts`; re-exported for what already imports them from here.
18
+ export { FOLDER_ORDER, MAIL_FOLDER_TYPES };
19
+ const NO_TRACKER = { settle: () => undefined, revert: () => undefined };
20
20
  /** What the persistent app frame (`AppChrome`) offers to the Mail shell so that it doesn't open a second connection. `null` outside a frame. */
21
21
  export const MailConnectionContext = createContext(null);
22
22
  /**
@@ -59,7 +59,7 @@ export function useMailConnection({ userUid, enabled, open }) {
59
59
  }
60
60
  setFoldersLoading(true);
61
61
  void Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
62
- .then((result) => ({ mailbox, folders: result.filter((f) => MAIL_FOLDER_TYPES.has(f.type)) }))
62
+ .then((result) => ({ mailbox, folders: result.filter(isMailFolder) }))
63
63
  .catch((err) => ({
64
64
  mailbox,
65
65
  folders: [],
@@ -69,17 +69,18 @@ export function useMailConnection({ userUid, enabled, open }) {
69
69
  .finally(() => setFoldersLoading(false));
70
70
  }, [mailboxes]);
71
71
  /** Files a newly created folder under its own mailbox, leaving every other mailbox's list untouched - and ignoring a type the
72
- * sidebar doesn't list at all, the same filter the fetch above applies. */
73
- const onFolderCreated = useCallback((folder) => {
74
- if (!MAIL_FOLDER_TYPES.has(folder.type)) {
75
- return;
76
- }
77
- setMailboxFolders((prev) => prev.map((entry) =>
78
- // Not one already there: another client's create event can arrive after (or twice, or alongside) our own.
79
- entry.mailbox.uid === folder.mailboxUid && !entry.folders.some((existing) => existing.uid === folder.uid)
80
- ? { ...entry, folders: [...entry.folders, folder] }
81
- : entry));
82
- }, []);
72
+ * sidebar doesn't list at all, the same filter the fetch above applies. One already there (another client's create event can arrive
73
+ * after, or twice, or alongside our own) is not added again - see `upsertFolder()`. */
74
+ const onFolderCreated = useCallback((folder) => setMailboxFolders((prev) => upsertFolder(prev, folder)), []);
75
+ /** A folder was renamed or changed type elsewhere. */
76
+ const onFolderChanged = useCallback((change) => setMailboxFolders((prev) => upsertFolder(prev, change)), []);
77
+ const onFolderDeleted = useCallback((folderUid) => setMailboxFolders((prev) => removeFolder(prev, folderUid)), []);
78
+ /** A mailbox's folders as the server just listed them (every count refresh does): folders the tree lacks - one created lazily on the
79
+ * first send, or by another client whose event never arrived - are added. */
80
+ const onFoldersListed = useCallback((listed) => setMailboxFolders((prev) => reconcileFolders(prev, listed)), []);
81
+ const askedFolderUidsRef = useRef(new Set());
82
+ const foldersRef = useRef(mailboxFolders);
83
+ foldersRef.current = mailboxFolders;
83
84
  // New mail without a reload: push events for every folder, plus a safety-net poll. Held in state beside `mailboxFolders` rather than
84
85
  // written into it, so a refreshed unread count never looks like a change of folders to the list on screen (which reloads, and forgets its
85
86
  // selection, whenever the folders do). A pop-up (and, in the background, a desktop notification) for each new message the connection
@@ -90,7 +91,48 @@ export function useMailConnection({ userUid, enabled, open }) {
90
91
  mailboxes,
91
92
  mailboxFolders,
92
93
  onFolderCreated,
94
+ onFolderChanged,
95
+ onFolderDeleted,
96
+ onFoldersListed,
93
97
  onMessageCreated: notifications.announce,
98
+ onSendEvent: handleSendEvent,
94
99
  });
95
- return { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, live, folderCounts, notifications };
100
+ const noteFolderUids = useCallback((folderUids) => {
101
+ const known = knownFolderUids(foldersRef.current);
102
+ const asked = askedFolderUidsRef.current;
103
+ let unknown = false;
104
+ for (const uid of folderUids) {
105
+ if (uid && !known.has(uid) && !asked.has(uid)) {
106
+ asked.add(uid);
107
+ unknown = true;
108
+ }
109
+ }
110
+ if (unknown) {
111
+ // The counts' read-back lists every mailbox's folders and files the new ones; a burst is one refresh.
112
+ folderCounts.refresh();
113
+ }
114
+ }, [folderCounts.refresh]);
115
+ // "Connection lost" for more than a few seconds is a subtle pop-up that goes when it is back.
116
+ usePushConnectionNotice(!!userUid && enabled);
117
+ // The Outbox indicator: a message being sent from this tab counts at once (before the server has heard of it), settled - or taken back - when
118
+ // the server answers (see `sendJob.ts`); once it accepted one, the folders are read again, since the Outbox may not have existed yet.
119
+ const latestRef = useRef({ mailboxFolders, folderCounts });
120
+ latestRef.current = { mailboxFolders, folderCounts };
121
+ useEffect(() => {
122
+ if (!userUid || !enabled) {
123
+ return;
124
+ }
125
+ return registerOutboxCounter((mailboxUid) => {
126
+ const { mailboxFolders: known, folderCounts: counts } = latestRef.current;
127
+ const outbox = known.find((entry) => entry.mailbox.uid === mailboxUid)?.folders.find((folder) => folder.type === "outbox");
128
+ return outbox ? counts.track(null, { folderUid: outbox.uid, flags: { read: true } }) : NO_TRACKER;
129
+ }, (mailboxUid) => {
130
+ // The server accepted a message: the Outbox (and, on an older server, Sent Items) may just have been made. The counts' read-back lists
131
+ // every mailbox's folders and files the new ones (`onFoldersListed`); this reads the one mailbox at once.
132
+ latestRef.current.folderCounts.refresh(0);
133
+ listFolders(mailboxUid).then(onFoldersListed, () => undefined);
134
+ });
135
+ }, [userUid, enabled, onFoldersListed]);
136
+ const outbox = useOutboxStatus(mailboxFolders, folderCounts.counts, live.tick, !!userUid && enabled);
137
+ return { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, notifications, outbox };
96
138
  }
@@ -1,6 +1,8 @@
1
1
  import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import { SendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
2
3
  import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
3
4
  import { FolderCounts } from "./folderCounts.js";
5
+ import { FolderChange } from "./folderTree.js";
4
6
  /** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
5
7
  * that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
6
8
  export declare const LIVE_POLL_INTERVAL_MS = 45000;
@@ -17,9 +19,10 @@ export interface LiveUpdates {
17
19
  export declare const NO_LIVE_UPDATES: LiveUpdates;
18
20
  /**
19
21
  * The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
20
- * mailbox's Inbox - where mail lands - then each mailbox's other folders in the sidebar's own order, then the mailbox uids
21
- * themselves (which announce new folders). A folder is a channel of its own; subscribing to a mailbox does not deliver its
22
- * folders' events.
22
+ * mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
23
+ * they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
24
+ * just appeared is filed by its type like any other, so when the server's cap bites it is the last of them that go without live
25
+ * updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
23
26
  */
24
27
  export declare function pushChannelsFor(mailboxFolders: MailboxFolders[], mailboxes: Mailbox[]): string[];
25
28
  export interface UseMailLiveUpdatesOptions {
@@ -29,10 +32,20 @@ export interface UseMailLiveUpdatesOptions {
29
32
  mailboxFolders: MailboxFolders[];
30
33
  /** Called with a folder another client (or this one) created, so the sidebar shows it. Called only for one not already known. */
31
34
  onFolderCreated: (folder: Folder) => void;
35
+ /** Called when a folder's name or type was changed elsewhere (a `Folder` update event that says so - one that only carries counts is not this). */
36
+ onFolderChanged?: (change: FolderChange) => void;
37
+ /** Called with the uid of a folder that was deleted elsewhere. */
38
+ onFolderDeleted?: (folderUid: string) => void;
39
+ /** Called with a mailbox's folders as the server just listed them - on every refresh (a message event, the poll, a reconnect, the tab
40
+ * coming back, a send) - so a folder the tree does not know is found whichever way it appeared. */
41
+ onFoldersListed?: (folders: Folder[]) => void;
32
42
  /** Called with each new message the push connection announces (a `create` event) - once per message, never for a list
33
43
  * refetch, the initial load or a reconnect (nothing published while the socket was down is replayed). The caller decides
34
44
  * whether it is worth telling the user about. */
35
45
  onMessageCreated?: (message: Message) => void;
46
+ /** Called with the outcome of a message sent in the background (`send-succeeded`, `send-failed`, `send-retrying`). Every one also
47
+ * refreshes what is on screen (the Outbox and Sent Items lists and counts change with it). */
48
+ onSendEvent?: (event: SendEvent) => void;
36
49
  }
37
50
  export interface MailLiveUpdates {
38
51
  live: LiveUpdates;
@@ -52,4 +65,4 @@ export interface MailLiveUpdates {
52
65
  * Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
53
66
  * Renders nothing and does nothing where there is no window (server-side rendering).
54
67
  */
55
- export declare function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated, onMessageCreated }: UseMailLiveUpdatesOptions): MailLiveUpdates;
68
+ export declare function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onFoldersListed, onMessageCreated, onSendEvent, }: UseMailLiveUpdatesOptions): MailLiveUpdates;
@@ -4,8 +4,10 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { useEffect, useMemo, useRef, useState } from "react";
6
6
  import { getPushClient } from "@rapidmx/react-shared/mail/pushClient.js";
7
+ import { parseSendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
7
8
  import { SIGN_OUT_CHANNEL } from "../search/localIndexRpcClient.js";
8
9
  import { useFolderCounts } from "./folderCounts.js";
10
+ import { sortFolders } from "./folderTree.js";
9
11
  /** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
10
12
  * that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
11
13
  export const LIVE_POLL_INTERVAL_MS = 45000;
@@ -18,19 +20,25 @@ const FOLDER_EVENT = /^Folder/;
18
20
  const MESSAGE_ACTIONS = new Set(["create", "update", "delete"]);
19
21
  /**
20
22
  * The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
21
- * mailbox's Inbox - where mail lands - then each mailbox's other folders in the sidebar's own order, then the mailbox uids
22
- * themselves (which announce new folders). A folder is a channel of its own; subscribing to a mailbox does not deliver its
23
- * folders' events.
23
+ * mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
24
+ * they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
25
+ * just appeared is filed by its type like any other, so when the server's cap bites it is the last of them that go without live
26
+ * updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
24
27
  */
25
28
  export function pushChannelsFor(mailboxFolders, mailboxes) {
26
29
  const inboxes = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "inbox"));
27
- const others = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type !== "inbox"));
28
- return [...new Set([...inboxes, ...others].map((folder) => folder.uid).concat(mailboxes.map((mailbox) => mailbox.uid)))];
30
+ const others = mailboxFolders.flatMap((entry) => sortFolders(entry.folders.filter((folder) => folder.type !== "inbox")));
31
+ return [...new Set([...inboxes.map((folder) => folder.uid), ...mailboxes.map((mailbox) => mailbox.uid), ...others.map((folder) => folder.uid)])];
29
32
  }
30
33
  function isFolder(value) {
31
34
  const folder = value;
32
35
  return !!folder && typeof folder.uid === "string" && typeof folder.mailboxUid === "string" && typeof folder.type === "string";
33
36
  }
37
+ /** A `Folder` update event that says the folder's name or type changed (and names the folder): the counts-only ones the server publishes after every message change do not. */
38
+ function isFolderChange(value) {
39
+ const change = value;
40
+ return !!change && typeof change.uid === "string" && (typeof change.name === "string" || typeof change.type === "string");
41
+ }
34
42
  function isMessage(value) {
35
43
  const message = value;
36
44
  return (!!message &&
@@ -59,13 +67,13 @@ function folderOfMessageEvent(event) {
59
67
  * Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
60
68
  * Renders nothing and does nothing where there is no window (server-side rendering).
61
69
  */
62
- export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated, onMessageCreated }) {
70
+ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onFoldersListed, onMessageCreated, onSendEvent, }) {
63
71
  const [live, setLive] = useState(NO_LIVE_UPDATES);
64
72
  const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
65
- const folderCounts = useFolderCounts(mailboxes, folders);
73
+ const folderCounts = useFolderCounts(mailboxes, folders, onFoldersListed);
66
74
  // The latest inputs, for the long-lived listeners below.
67
- const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onMessageCreated, folderCounts });
68
- latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onMessageCreated, folderCounts };
75
+ const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts });
76
+ latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts };
69
77
  const channels = useMemo(() => pushChannelsFor(mailboxFolders, mailboxes), [mailboxFolders, mailboxes]);
70
78
  const channelKey = channels.join("|");
71
79
  const channelsRef = useRef(channels);
@@ -103,7 +111,13 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
103
111
  timer ?? (timer = setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS));
104
112
  }
105
113
  const offEvent = client.onEvent((event) => {
106
- if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
114
+ const sendEvent = parseSendEvent(event);
115
+ if (sendEvent) {
116
+ // A queued message was relayed (or failed to be): it left Outbox for Sent Items, or stayed there failed - every list and count may have changed.
117
+ latestRef.current.onSendEvent?.(sendEvent);
118
+ schedule();
119
+ }
120
+ else if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
107
121
  if (event.action === "create" && isMessage(event.data)) {
108
122
  // Once per message, whatever else the event triggers: the badge first, then whoever announces it.
109
123
  if (latestRef.current.folderCounts.noteCreated(event.data)) {
@@ -113,15 +127,27 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
113
127
  schedule(folderOfMessageEvent(event));
114
128
  }
115
129
  else if (FOLDER_EVENT.test(event.type) && event.action === "create" && isFolder(event.data)) {
130
+ // A folder made by the server (on first use, or by another client) or by this one: filed in the sidebar and subscribed to at once (the
131
+ // channel list follows the tree), and its real counts read - all without a reload.
116
132
  const folder = event.data;
117
133
  const known = latestRef.current.mailboxFolders.some((entry) => entry.folders.some((f) => f.uid === folder.uid));
118
134
  if (!known) {
119
135
  latestRef.current.onFolderCreated(folder);
136
+ latestRef.current.folderCounts.refresh(0);
120
137
  }
121
138
  }
122
139
  else if (FOLDER_EVENT.test(event.type) && event.action === "update") {
140
+ if (isFolderChange(event.data)) {
141
+ latestRef.current.onFolderChanged?.(event.data);
142
+ }
123
143
  latestRef.current.folderCounts.applyFolderEvent(event.data);
124
144
  }
145
+ else if (FOLDER_EVENT.test(event.type) && event.action === "delete") {
146
+ const uid = event.data?.uid;
147
+ if (typeof uid === "string") {
148
+ latestRef.current.onFolderDeleted?.(uid);
149
+ }
150
+ }
125
151
  });
126
152
  // Anything published while the socket was down is gone for good, so every reconnect is followed by a refresh.
127
153
  const offStatus = client.onStatus((status) => {
@@ -1,19 +1,12 @@
1
1
  import type { Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
2
  import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
3
- import { NewMailNotice } from "./newMailNotifications.js";
4
- /** The most pop-ups on screen at once; a new one pushes the oldest off. */
5
- export declare const MAX_TOASTS = 3;
6
- /** How long a pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
3
+ /** How long a new-mail pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
7
4
  export declare const TOAST_DURATION_MS = 8000;
8
5
  /** At most this many desktop notifications in `DESKTOP_BURST_WINDOW_MS`: a flood of mail is a few notifications, not a hundred. */
9
6
  export declare const DESKTOP_BURST_LIMIT = 5;
10
7
  export declare const DESKTOP_BURST_WINDOW_MS = 30000;
11
8
  export interface NewMailNotifications {
12
- /** The pop-ups on screen, oldest first. */
13
- toasts: NewMailNotice[];
14
- /** Takes one off the screen. */
15
- dismiss(uid: string): void;
16
- /** Whether the first pop-up should also offer to turn on desktop notifications: the browser can, has not been asked,
9
+ /** Whether the next pop-up should also offer to turn on desktop notifications: the browser can, has not been asked,
17
10
  * and the user has not said "Not now". */
18
11
  offerDesktop: boolean;
19
12
  /** The offer's "Turn on desktop notifications": asks the browser. Must be called from a click. */
@@ -30,7 +23,8 @@ export interface UseNewMailNotificationsOptions {
30
23
  open?: (href: string) => void;
31
24
  }
32
25
  /**
33
- * New-mail announcements: an in-app pop-up for each message that arrives (`MAX_TOASTS` at most on screen), and - while the tab
26
+ * New-mail announcements: an in-app pop-up for each message that arrives (a `mail` notification - see `notifications/store.ts`, which owns the
27
+ * stack, its limit of three and the clock), and - while the tab
34
28
  * is in the background and the user has allowed it - a desktop notification with the same content, one per message however
35
29
  * many events name it. `announce()` is what `useMailLiveUpdates()`'s `onMessageCreated` calls; nothing else feeds it, so a
36
30
  * page load, a list refetch and a reconnect never announce anything.
@@ -4,9 +4,8 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { desktopPermission, getDesktopOfferDismissed, getNewMailPopupsEnabled, noticeFor, noticeSender, ownAddressesOf, requestDesktopPermission, setDesktopOfferDismissed, shouldAnnounce, } from "./newMailNotifications.js";
7
- /** The most pop-ups on screen at once; a new one pushes the oldest off. */
8
- export const MAX_TOASTS = 3;
9
- /** How long a pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
7
+ import { getNotificationsSnapshot, notify } from "../notifications/store.js";
8
+ /** How long a new-mail pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
10
9
  export const TOAST_DURATION_MS = 8000;
11
10
  /** At most this many desktop notifications in `DESKTOP_BURST_WINDOW_MS`: a flood of mail is a few notifications, not a hundred. */
12
11
  export const DESKTOP_BURST_LIMIT = 5;
@@ -21,7 +20,8 @@ function defaultOpen(href) {
21
20
  window.location.href = href;
22
21
  }
23
22
  /**
24
- * New-mail announcements: an in-app pop-up for each message that arrives (`MAX_TOASTS` at most on screen), and - while the tab
23
+ * New-mail announcements: an in-app pop-up for each message that arrives (a `mail` notification - see `notifications/store.ts`, which owns the
24
+ * stack, its limit of three and the clock), and - while the tab
25
25
  * is in the background and the user has allowed it - a desktop notification with the same content, one per message however
26
26
  * many events name it. `announce()` is what `useMailLiveUpdates()`'s `onMessageCreated` calls; nothing else feeds it, so a
27
27
  * page load, a list refetch and a reconnect never announce anything.
@@ -30,7 +30,6 @@ function defaultOpen(href) {
30
30
  * first pop-up, or from Settings. What the user answered is remembered in `localStorage` (see `newMailNotifications.ts`).
31
31
  */
32
32
  export function useNewMailNotifications({ mailboxes, mailboxFolders, open = defaultOpen }) {
33
- const [toasts, setToasts] = useState([]);
34
33
  const [permission, setPermission] = useState("unsupported");
35
34
  const [offerDismissed, setOfferDismissed] = useState(true);
36
35
  const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
@@ -44,9 +43,6 @@ export function useNewMailNotifications({ mailboxes, mailboxFolders, open = defa
44
43
  setPermission(desktopPermission());
45
44
  setOfferDismissed(getDesktopOfferDismissed());
46
45
  }, []);
47
- const dismiss = useCallback((uid) => {
48
- setToasts((previous) => previous.filter((toast) => toast.uid !== uid));
49
- }, []);
50
46
  const showDesktop = useCallback((notice) => {
51
47
  const now = Date.now();
52
48
  desktopShownRef.current = desktopShownRef.current.filter((at) => now - at < DESKTOP_BURST_WINDOW_MS);
@@ -70,6 +66,21 @@ export function useNewMailNotifications({ mailboxes, mailboxFolders, open = defa
70
66
  // Some browsers refuse `new Notification()` outright (a mobile one wanting a service worker): the pop-up is enough.
71
67
  }
72
68
  }, []);
69
+ const enableDesktop = useCallback(async () => {
70
+ const answer = await requestDesktopPermission();
71
+ setPermission(answer);
72
+ // Answered either way, so the offer goes: on granted it has done its job, on denied it can't be made again.
73
+ setDesktopOfferDismissed(true);
74
+ setOfferDismissed(true);
75
+ }, []);
76
+ const declineDesktop = useCallback(() => {
77
+ setDesktopOfferDismissed(true);
78
+ setOfferDismissed(true);
79
+ }, []);
80
+ // The offer's current state and answers, for the stable `announce` below.
81
+ const offerDesktop = permission === "default" && !offerDismissed;
82
+ const offerRef = useRef({ offered: offerDesktop, enable: enableDesktop, decline: declineDesktop });
83
+ offerRef.current = { offered: offerDesktop, enable: enableDesktop, decline: declineDesktop };
73
84
  const announce = useCallback((message) => {
74
85
  if (!getNewMailPopupsEnabled() || announcedRef.current.has(message.uid)) {
75
86
  return;
@@ -83,26 +94,35 @@ export function useNewMailNotifications({ mailboxes, mailboxFolders, open = defa
83
94
  announcedRef.current.delete(announcedRef.current.values().next().value);
84
95
  }
85
96
  const notice = noticeFor(message);
86
- setToasts((previous) => [...previous.filter((toast) => toast.uid !== notice.uid), notice].slice(-MAX_TOASTS));
97
+ const offer = offerRef.current;
98
+ notify({
99
+ id: `mail:${notice.uid}`,
100
+ kind: "mail",
101
+ title: notice.senderName || notice.senderAddress,
102
+ subtitle: notice.senderName ? `<${notice.senderAddress}>` : undefined,
103
+ message: notice.subject,
104
+ preview: notice.preview || undefined,
105
+ href: notice.href,
106
+ timeoutMs: TOAST_DURATION_MS,
107
+ // Actions make a notification sticky; a new-mail pop-up with the offer still goes by itself.
108
+ sticky: false,
109
+ // The offer is made once at a time: on a pop-up only while none of the pop-ups still showing carries it.
110
+ ...(offer.offered && !getNotificationsSnapshot().visible.some((item) => item.kind === "mail" && item.actions.length > 0)
111
+ ? {
112
+ hint: "Get a desktop notification when mail arrives while this tab is in the background.",
113
+ actions: [
114
+ { label: "Turn on desktop notifications", onClick: () => void offer.enable() },
115
+ { label: "Not now", onClick: offer.decline },
116
+ ],
117
+ }
118
+ : {}),
119
+ });
87
120
  if (desktopPermission() === "granted" && tabIsInBackground()) {
88
121
  showDesktop(notice);
89
122
  }
90
123
  }, [showDesktop]);
91
- const enableDesktop = useCallback(async () => {
92
- const answer = await requestDesktopPermission();
93
- setPermission(answer);
94
- // Answered either way, so the offer goes: on granted it has done its job, on denied it can't be made again.
95
- setDesktopOfferDismissed(true);
96
- setOfferDismissed(true);
97
- }, []);
98
- const declineDesktop = useCallback(() => {
99
- setDesktopOfferDismissed(true);
100
- setOfferDismissed(true);
101
- }, []);
102
124
  return {
103
- toasts,
104
- dismiss,
105
- offerDesktop: permission === "default" && !offerDismissed,
125
+ offerDesktop,
106
126
  enableDesktop,
107
127
  declineDesktop,
108
128
  announce,
@@ -46,8 +46,8 @@ import { matchRoute } from "./routes.js";
46
46
  import { RouterContext, UNKNOWN_LOCATION, readWindowLocation } from "./routerContext.js";
47
47
  export { useLocation, useLocationSearch, useNavigate } from "./routerContext.js";
48
48
  function pickChromeProps(props) {
49
- const { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav } = props;
50
- return { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav };
49
+ const { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance } = props;
50
+ return { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance };
51
51
  }
52
52
  /** The anchor a click/pointer event is on or in, if it is a link. */
53
53
  function linkOf(event) {
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ /**
3
+ * The pop-up stack, and the one place the app's notifications (see `store.ts`) are drawn. Mounted once, in the persistent app frame, so
4
+ * it survives page changes and covers every app.
5
+ *
6
+ * **Where it sits.** Top right, *below* the header row: the stack is anchored to a zero-height `sticky` line placed right after the
7
+ * frame's header, so it starts under the header (and so under the account menu button, whatever height a branded header has) and, as
8
+ * the page scrolls, sticks just below the header (`--rr-header-h`, published by the title bar / `FrameBrandingHeader` - see `headerOffset.ts`;
9
+ * never less than 2.5 rem, which clears a full-screen compose sheet's own title bar on a phone).
10
+ * On a desktop the compose windows are anchored at the bottom right, the far end of the screen from this; it never covers the account menu,
11
+ * the header's actions or a compose window's title bar and Send button unless the window is stretched over most of the screen height.
12
+ * It sits above the compose sheet (z-index 60 against 50), so a pop-up is never hidden behind it. Its height is capped and scrolls.
13
+ *
14
+ * **Live regions.** Two always-mounted containers with `aria-live` (a live region has to exist before its content changes to be announced):
15
+ * errors in an assertive one, everything else in a polite one, and each pop-up carries the matching role - `alert` for an error, `status`
16
+ * for the rest - so it is announced accordingly, and the empty containers add no `alert`/`status` of their own to the page.
17
+ * `prefers-reduced-motion` switches the entrance animation off (`.rr-toast-in`).
18
+ */
19
+ export default function NotificationCenter(): React.JSX.Element;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect } from "react";
7
+ import { HiOutlineCheckCircle, HiOutlineEnvelope, HiOutlineExclamationCircle, HiOutlineExclamationTriangle, HiOutlineInformationCircle, HiOutlineXMark, } from "react-icons/hi2";
8
+ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
9
+ import { dismiss, setAllPaused, setPaused } from "./store.js";
10
+ import { useNotifications } from "./useNotifications.js";
11
+ import { useSystemErrorNotifications } from "./systemErrors.js";
12
+ const KIND_STYLE = {
13
+ mail: { border: "border-l-primary", icon: "text-primary-dark", Icon: HiOutlineEnvelope, label: "New message" },
14
+ info: { border: "border-l-primary", icon: "text-primary-dark", Icon: HiOutlineInformationCircle, label: "Information" },
15
+ success: { border: "border-l-success", icon: "text-success", Icon: HiOutlineCheckCircle, label: "Success" },
16
+ warning: { border: "border-l-warning", icon: "text-warning", Icon: HiOutlineExclamationTriangle, label: "Warning" },
17
+ error: { border: "border-l-danger", icon: "text-danger", Icon: HiOutlineExclamationCircle, label: "Error" },
18
+ };
19
+ const ACTION_CLASS = "rounded-sm px-2.5 py-1 text-xs font-semibold text-primary-dark hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary";
20
+ /** Stops every clock while the tab is out of view: nobody could have read what is on screen, and it is still there when they come back. */
21
+ function usePauseWhileHidden() {
22
+ useEffect(() => {
23
+ const update = () => setAllPaused(document.visibilityState === "hidden");
24
+ update();
25
+ document.addEventListener("visibilitychange", update);
26
+ return () => {
27
+ document.removeEventListener("visibilitychange", update);
28
+ setAllPaused(false);
29
+ };
30
+ }, []);
31
+ }
32
+ function ActionButton({ action, id }) {
33
+ function run() {
34
+ action.onClick?.();
35
+ if (!action.keepOpen) {
36
+ dismiss(id);
37
+ }
38
+ }
39
+ return action.href ? (_jsx("a", { href: action.href, onClick: run, className: ACTION_CLASS, children: action.label })) : (_jsx("button", { type: "button", onClick: run, className: ACTION_CLASS, children: action.label }));
40
+ }
41
+ /** The expandable, copyable technical lines behind a notification. */
42
+ function Details({ lines }) {
43
+ return (_jsxs("details", { className: "mt-1.5", children: [_jsx("summary", { className: "cursor-pointer text-xs font-semibold text-text-muted", children: "Technical details" }), _jsx("ul", { "aria-label": "Technical details", className: "mt-1.5 max-h-40 overflow-auto rounded-sm border border-border bg-surface-alt p-2 font-mono text-xs text-text select-text", children: lines.map((line, index) => (_jsx("li", { className: "whitespace-pre-wrap break-words", children: line }, index))) }), _jsx("div", { className: "mt-1.5", children: _jsx(CopyButton, { value: lines.join("\n"), label: "Copy technical details" }) })] }));
44
+ }
45
+ /** One pop-up. Its clock (kept by the store) stops while it is hovered or holds the keyboard focus; Escape dismisses it. */
46
+ function NotificationItem({ notification }) {
47
+ const { id, kind, title, subtitle, message, preview, hint, details, actions, href, count } = notification;
48
+ const style = KIND_STYLE[kind];
49
+ const body = (_jsxs(_Fragment, { children: [kind === "mail" && _jsx("span", { className: "block text-xs font-semibold uppercase tracking-wide text-primary-dark", children: style.label }), _jsxs("span", { className: "flex min-w-0 items-baseline gap-1.5 text-sm", children: [_jsx("span", { className: ["truncate font-bold", kind === "error" ? "text-danger" : ""].join(" "), children: title }), count > 1 && (_jsxs("span", { className: "shrink-0 rounded-pill bg-surface-alt px-1.5 text-xs font-semibold text-text-muted", children: [_jsxs("span", { "aria-hidden": "true", children: ["\u00D7", count] }), _jsxs("span", { className: "sr-only", children: [" (", count, " times)"] })] })), subtitle && _jsx("span", { className: "truncate text-xs text-text-muted", children: subtitle })] }), message && _jsx("span", { className: ["mt-0.5 block text-sm [overflow-wrap:anywhere]", kind === "mail" ? "truncate font-semibold" : ""].join(" "), children: message }), preview && _jsx("span", { className: "mt-0.5 block text-xs text-text-muted [overflow-wrap:anywhere]", children: preview })] }));
50
+ return (_jsxs("div", { "data-testid": "notification", role: kind === "error" ? "alert" : "status", "data-kind": kind, "data-notification-id": id, onMouseEnter: () => setPaused(id, true), onMouseLeave: () => setPaused(id, false), onFocus: () => setPaused(id, true), onBlur: (event) => {
51
+ // Focus moving between this pop-up's own controls is not leaving it.
52
+ if (!event.currentTarget.contains(event.relatedTarget)) {
53
+ setPaused(id, false);
54
+ }
55
+ }, onKeyDown: (event) => {
56
+ if (event.key === "Escape") {
57
+ event.preventDefault();
58
+ dismiss(id);
59
+ }
60
+ }, className: `rr-toast-in pointer-events-auto relative rounded-md border border-border border-l-4 ${style.border} bg-surface text-text shadow-lg`, children: [_jsxs("div", { className: "flex items-start gap-2 p-3 pr-10", children: [kind !== "mail" && _jsx(style.Icon, { size: 18, "aria-hidden": "true", className: `mt-0.5 shrink-0 ${style.icon}` }), _jsxs("div", { className: "min-w-0 flex-1", children: [href ? (_jsx("a", { href: href, onClick: () => dismiss(id), className: "-m-1 block rounded-md p-1 hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary", children: body })) : (body), hint && _jsx("p", { className: "mt-1.5 text-xs text-text-muted", children: hint }), details.length > 0 && _jsx(Details, { lines: details }), actions.length > 0 && (_jsx("div", { className: "-ml-2.5 mt-1.5 flex flex-wrap items-center gap-1", children: actions.map((action, index) => (_jsx(ActionButton, { action: action, id: id }, index))) }))] })] }), _jsx("button", { type: "button", onClick: () => dismiss(id), "aria-label": `Dismiss notification: ${title}`, className: "absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }));
61
+ }
62
+ /**
63
+ * The pop-up stack, and the one place the app's notifications (see `store.ts`) are drawn. Mounted once, in the persistent app frame, so
64
+ * it survives page changes and covers every app.
65
+ *
66
+ * **Where it sits.** Top right, *below* the header row: the stack is anchored to a zero-height `sticky` line placed right after the
67
+ * frame's header, so it starts under the header (and so under the account menu button, whatever height a branded header has) and, as
68
+ * the page scrolls, sticks just below the header (`--rr-header-h`, published by the title bar / `FrameBrandingHeader` - see `headerOffset.ts`;
69
+ * never less than 2.5 rem, which clears a full-screen compose sheet's own title bar on a phone).
70
+ * On a desktop the compose windows are anchored at the bottom right, the far end of the screen from this; it never covers the account menu,
71
+ * the header's actions or a compose window's title bar and Send button unless the window is stretched over most of the screen height.
72
+ * It sits above the compose sheet (z-index 60 against 50), so a pop-up is never hidden behind it. Its height is capped and scrolls.
73
+ *
74
+ * **Live regions.** Two always-mounted containers with `aria-live` (a live region has to exist before its content changes to be announced):
75
+ * errors in an assertive one, everything else in a polite one, and each pop-up carries the matching role - `alert` for an error, `status`
76
+ * for the rest - so it is announced accordingly, and the empty containers add no `alert`/`status` of their own to the page.
77
+ * `prefers-reduced-motion` switches the entrance animation off (`.rr-toast-in`).
78
+ */
79
+ export default function NotificationCenter() {
80
+ const { visible, queued } = useNotifications();
81
+ usePauseWhileHidden();
82
+ useSystemErrorNotifications();
83
+ const errors = visible.filter((notification) => notification.kind === "error");
84
+ const others = visible.filter((notification) => notification.kind !== "error");
85
+ return (_jsx("div", { "data-testid": "notification-anchor", className: "sticky top-[max(var(--rr-header-h,0px),2.5rem)] z-[60] h-0 w-full", children: _jsxs("div", { "data-testid": "notification-stack",
86
+ // Never taller than the room above the open compose windows (`ComposeProvider` publishes their top edge as `--rr-compose-top`), so
87
+ // a stack of pop-ups scrolls inside its own box rather than covering a compose window's title bar and buttons.
88
+ // (The padding is room for the pop-ups' shadow, which the scroll box would otherwise cut off in a visible edge.)
89
+ className: "pointer-events-none absolute right-0 -top-1 flex max-h-[max(8rem,calc(var(--rr-compose-top,100vh)-max(var(--rr-header-h,0px),2.5rem)-2rem))] w-[25rem] max-w-[calc(100vw-0.5rem)] flex-col overflow-y-auto p-4", children: [_jsx("div", { "data-testid": "notification-errors", "aria-live": "assertive", className: "flex flex-col gap-2 [&:not(:empty)]:mb-2", children: errors.map((notification) => (_jsx(NotificationItem, { notification: notification }, notification.id))) }), _jsxs("div", { "data-testid": "notification-others", "aria-live": "polite", className: "flex flex-col gap-2", children: [others.map((notification) => (_jsx(NotificationItem, { notification: notification }, notification.id))), queued > 0 && (_jsxs("p", { className: "rr-solid pointer-events-auto self-end rounded-pill bg-surface px-2.5 py-0.5 text-xs text-text-muted shadow-md", children: [queued, " more waiting"] }))] })] }) }));
90
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ export interface NotificationHistoryDialogProps {
3
+ open: boolean;
4
+ onClose: () => void;
5
+ }
6
+ /**
7
+ * "Recent notifications": the last few pop-ups, newest first, so nothing is lost when one expires. Opening it counts as having seen
8
+ * them (the unseen-error count on the account menu goes), but the errors that were unseen when it opened stay highlighted until it closes.
9
+ */
10
+ export default function NotificationHistoryDialog({ open, onClose }: NotificationHistoryDialogProps): React.JSX.Element;