@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
@@ -6,15 +6,14 @@ import { routedPage } from "../../_routedPage.js";
6
6
  import React, { FormEvent, useEffect, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import {
9
- EnrollmentResult,
10
9
  KeyVault,
11
10
  MasterKeyWrap,
12
11
  PublicKey,
13
12
  WrappedPrivateKey,
14
13
  addMasterKeyWrap,
15
14
  cancelSignEnrollment,
16
- checkSignEnrollmentStatus,
17
15
  findActivePublicKey,
16
+ getCurrentSignEnrollment,
18
17
  getEscrowInfo,
19
18
  getKeyVault,
20
19
  rekey,
@@ -45,9 +44,22 @@ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../sh
45
44
  import KeyEnrollmentGate from "../../../shared/components/layout/KeyEnrollmentGate.js";
46
45
  import { useUnlockPrompt } from "../../../shared/components/layout/UnlockPromptProvider.js";
47
46
  import { destroyLocalIndex } from "../../../shared/search/localIndexRpcClient.js";
47
+ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
48
48
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
49
49
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
50
50
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
51
+ import SigningCertificateCard, { SigningCertificateMode } from "../../../shared/components/settings/SigningCertificateCard.js";
52
+ import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
53
+ import {
54
+ checkEnrollmentNow,
55
+ forgetEnrollment,
56
+ recordEnrollmentResult,
57
+ useEnrollmentSnapshot,
58
+ watchCurrentEnrollment,
59
+ watchEnrollment,
60
+ } from "../../../shared/signing/enrollmentTracker.js";
61
+ import { STALE_REQUEST_TEXT, beforeRequestText, healthWarning, isInstalling, timeOf } from "../../../shared/signing/enrollmentView.js";
62
+ import { useSigningEnrollmentInfo } from "../../../shared/signing/signingInfo.js";
51
63
 
52
64
  const MIN_PASSWORD_LENGTH = 8;
53
65
 
@@ -55,6 +67,10 @@ const MIN_PASSWORD_LENGTH = 8;
55
67
  * already holding this many wraps (escrow included). */
56
68
  export const MAX_MASTER_KEY_WRAPS = 20;
57
69
 
70
+ /** How often, and how many times, the page asks for the mailbox's keys while a certificate is issued but not installed yet. */
71
+ const INSTALL_WATCH_INTERVAL_MS = 15_000;
72
+ const INSTALL_WATCH_ATTEMPTS = 40;
73
+
58
74
  const KEYS_LOCKED_MESSAGE = "Your encryption keys were locked before this could finish. Unlock them and try again.";
59
75
 
60
76
  /** Thrown inside `handleRotateKeys()` when the escrow wrap of the new master key can't be built - the rotation is
@@ -160,39 +176,6 @@ async function rewrapVaultPrivateKeys(
160
176
  }
161
177
  }
162
178
 
163
- // RFC 8823 ACME issuance is a real email round-trip with a public CA - "likely minutes," not seconds -
164
- // so this polls infrequently rather than hammering the endpoint.
165
- const SIGNING_ENROLLMENT_POLL_INTERVAL_MS = 15_000;
166
-
167
- /**
168
- * A started signing enrollment's id, per mailbox. Kept only so a reload can ask the server
169
- * (`checkSignEnrollmentStatus()`) whether that enrollment is still pending: a rotation while it is pending
170
- * would strand the enrollment's already-submitted private key under a master key that no longer exists. The
171
- * stored id is never trusted on its own - the server's answer decides the state, and restapi's own `rekey()`
172
- * refuses (409) a rotation during an enrollment this browser never saw (e.g. one started on another device).
173
- */
174
- const SIGN_ENROLLMENT_STORAGE_PREFIX = "rapidmx.signEnrollment.";
175
-
176
- function readStoredSignEnrollment(mailboxUid: string): string | null {
177
- try {
178
- return localStorage.getItem(SIGN_ENROLLMENT_STORAGE_PREFIX + mailboxUid);
179
- } catch {
180
- return null;
181
- }
182
- }
183
-
184
- function storeSignEnrollment(mailboxUid: string, enrollmentId: string | null): void {
185
- try {
186
- if (enrollmentId) {
187
- localStorage.setItem(SIGN_ENROLLMENT_STORAGE_PREFIX + mailboxUid, enrollmentId);
188
- } else {
189
- localStorage.removeItem(SIGN_ENROLLMENT_STORAGE_PREFIX + mailboxUid);
190
- }
191
- } catch {
192
- // Storage blocked - restapi's own 409 on rekey still guards a rotation.
193
- }
194
- }
195
-
196
179
  const ROTATION_CONFLICT_MESSAGE =
197
180
  "Your keys were not rotated because the server reported a conflicting change. If a signing certificate enrollment is still in progress for this mailbox (it may have been started on another device), wait for it to finish, then try again. If this mailbox is under escrow, its escrow protection may have changed meanwhile - reload this page and try again. Nothing was changed.";
198
181
 
@@ -304,6 +287,12 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
304
287
  const [signingEnrollmentId, setSigningEnrollmentId] = useState<string | null>(null);
305
288
  const [signingError, setSigningError] = useState<string | null>(null);
306
289
  const rotationBlockedBySigning = signingStatus !== "idle";
290
+ // What is known of the mailbox's signing-certificate enrollment: kept current by the shared tracker (`signing/enrollmentTracker.ts`), which reads
291
+ // the server with backoff while this page is visible, stops when the enrollment ends, and goes on in the app frame when this page is left.
292
+ const enrollment = useEnrollmentSnapshot(mailboxUid);
293
+ const enrollmentResult = enrollment?.result ?? null;
294
+ // How this deployment issues certificates (a public CA by itself, or an administrator by hand): what the card and the request say follows it. Only the owner asks.
295
+ const signingInfo = useSigningEnrollmentInfo(canManageKeys);
307
296
 
308
297
  const [cancelingEnrollment, setCancelingEnrollment] = useState(false);
309
298
  const [cancelEnrollmentError, setCancelEnrollmentError] = useState<string | null>(null);
@@ -377,79 +366,80 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
377
366
  }
378
367
  }
379
368
 
380
- /** Applies a server-reported enrollment status - shared by the reload check and the poll below. */
381
- async function applyEnrollmentResult(enrollmentId: string, result: EnrollmentResult, isCancelled: () => boolean) {
382
- if (result.status === "pending") {
383
- setSigningEnrollmentId(enrollmentId);
384
- setSigningStatus("pending");
385
- return;
386
- }
387
- storeSignEnrollment(mailboxUid!, null);
388
- setSigningStatus("idle");
389
- setSigningEnrollmentId(null);
390
- if (result.status === "failed") {
391
- setSigningError(result.error ?? "Signing certificate enrollment failed.");
392
- return;
393
- }
394
- try {
395
- const refreshed = await getMailbox(mailboxUid!);
396
- if (!isCancelled()) {
397
- setRefreshedKeys(refreshed.keys ?? []);
398
- }
399
- } catch {
400
- // The new key shows on the next load - the enrollment itself is finished either way.
401
- }
402
- }
403
-
404
- // Confirms an enrollment started before a reload with the server before rotation is offered again.
369
+ // Follows the enrollment this browser started (its stored id) - or, for the owner, the one the server names as the mailbox's current one (started
370
+ // on another device, or long ago), so a pending one blocks rotation and shows its progress here too.
405
371
  useEffect(() => {
406
372
  const storedId = readStoredSignEnrollment(mailboxUid!);
407
- if (!storedId) {
408
- return;
409
- }
410
373
  let cancelled = false;
411
- setSigningStatus("checking");
412
- checkSignEnrollmentStatus(mailboxUid!, storedId)
413
- .then((result) => (cancelled ? undefined : applyEnrollmentResult(storedId, result, () => cancelled)))
414
- .catch((err) => {
415
- if (cancelled) {
416
- return;
417
- }
418
- if (err instanceof ApiRequestError && err.status === 404) {
419
- // The server no longer knows this enrollment, so nothing is pending.
420
- storeSignEnrollment(mailboxUid!, null);
421
- setSigningStatus("idle");
422
- return;
423
- }
424
- // Unknown (e.g. a network error): assume it is still pending, and let polling keep asking.
425
- setSigningEnrollmentId(storedId);
426
- setSigningStatus("pending");
427
- });
374
+ let release: (() => void) | undefined;
375
+ if (storedId) {
376
+ setSigningStatus("checking");
377
+ release = watchEnrollment(mailboxUid!, storedId);
378
+ } else if (canManageKeys) {
379
+ getCurrentSignEnrollment(mailboxUid!)
380
+ .then((current) => {
381
+ if (!cancelled && current) {
382
+ if (current.status === "pending") {
383
+ storeSignEnrollment(mailboxUid!, current.enrollmentId);
384
+ }
385
+ release = watchCurrentEnrollment(mailboxUid!, current, current.status === "pending");
386
+ }
387
+ })
388
+ .catch(() => undefined);
389
+ }
428
390
  return () => {
429
391
  cancelled = true;
392
+ release?.();
430
393
  };
431
- }, [mailboxUid]);
394
+ }, [mailboxUid, canManageKeys]);
432
395
 
396
+ // What the tracker knows becomes this page's state: pending (or not heard from yet - assume it is still going, and let the polling keep asking) blocks
397
+ // rotation; an enrollment that ended, or that the server no longer knows, does not.
433
398
  useEffect(() => {
434
- if (signingStatus !== "pending" || !signingEnrollmentId) {
399
+ if (!enrollment) {
400
+ return;
401
+ }
402
+ if (enrollmentResult?.status === "pending" || (!enrollmentResult && enrollment.offline)) {
403
+ setSigningEnrollmentId(enrollment.enrollmentId);
404
+ setSigningStatus("pending");
405
+ } else if (enrollmentResult || enrollment.gone) {
406
+ setSigningEnrollmentId(null);
407
+ setSigningStatus("idle");
408
+ }
409
+ }, [enrollment?.enrollmentId, enrollmentResult?.status, enrollment?.gone, enrollment?.offline]);
410
+
411
+ // Once a certificate is issued the server has installed a new signing key that `mailbox.keys` (fetched once by the shell) never saw. The server installs it
412
+ // with a job a few minutes after the CA issues it (`installedAt`): until then the key is not there, so ask again every 15 s (for ten minutes at most).
413
+ const issuedUntil = timeOf(enrollmentResult?.notAfter);
414
+ const keyIsCurrent = !!activeSigningKey && (issuedUntil === undefined || issuedUntil <= activeSigningKey.notAfter + 60_000);
415
+ useEffect(() => {
416
+ if (enrollmentResult?.status !== "issued" || keyIsCurrent) {
435
417
  return;
436
418
  }
437
419
  let cancelled = false;
438
- const interval = setInterval(async () => {
439
- try {
440
- const result = await checkSignEnrollmentStatus(mailboxUid!, signingEnrollmentId);
441
- if (!cancelled) {
442
- await applyEnrollmentResult(signingEnrollmentId, result, () => cancelled);
443
- }
444
- } catch {
445
- // Transient network error - keep polling rather than surfacing a one-off failure.
446
- }
447
- }, SIGNING_ENROLLMENT_POLL_INTERVAL_MS);
420
+ let timer: ReturnType<typeof setTimeout> | undefined;
421
+ let attempts = 0;
422
+ const refresh = () => {
423
+ getMailbox(mailboxUid!)
424
+ .then((refreshed) => {
425
+ if (cancelled) {
426
+ return;
427
+ }
428
+ const keys = refreshed.keys ?? [];
429
+ setRefreshedKeys(keys);
430
+ // Not installed yet, and the server said it is on its way: look again.
431
+ if (isInstalling(enrollmentResult) && ++attempts < INSTALL_WATCH_ATTEMPTS && !findActivePublicKey(keys, "sign")) {
432
+ timer = setTimeout(refresh, INSTALL_WATCH_INTERVAL_MS);
433
+ }
434
+ })
435
+ .catch(() => undefined);
436
+ };
437
+ refresh();
448
438
  return () => {
449
439
  cancelled = true;
450
- clearInterval(interval);
440
+ clearTimeout(timer);
451
441
  };
452
- }, [signingStatus, signingEnrollmentId, mailboxUid]);
442
+ }, [enrollmentResult?.status, enrollmentResult?.installedAt, enrollment?.enrollmentId, keyIsCurrent, mailboxUid]);
453
443
 
454
444
  /** Cancels the pending enrollment (restapi's owner-only DELETE), which is what lets rotation run again. */
455
445
  async function handleCancelEnrollment(enrollmentId: string) {
@@ -461,16 +451,18 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
461
451
  setCancelEnrollmentError("The enrollment couldn't be cancelled yet. Try again.");
462
452
  } else if (result.status === "failed") {
463
453
  // Cancelled - the "failure" is the cancellation itself, not something to report.
454
+ forgetEnrollment(mailboxUid!);
464
455
  storeSignEnrollment(mailboxUid!, null);
465
456
  setSigningStatus("idle");
466
457
  setSigningEnrollmentId(null);
467
458
  } else {
468
459
  // Issued before the cancel landed: it finishes like any other issued enrollment.
469
- await applyEnrollmentResult(enrollmentId, result, () => false);
460
+ recordEnrollmentResult(mailboxUid!, enrollmentId, result);
470
461
  }
471
462
  } catch (err) {
472
463
  if (err instanceof ApiRequestError && err.status === 404) {
473
464
  // The server no longer knows this enrollment, so nothing is pending.
465
+ forgetEnrollment(mailboxUid!);
474
466
  storeSignEnrollment(mailboxUid!, null);
475
467
  setSigningStatus("idle");
476
468
  setSigningEnrollmentId(null);
@@ -499,6 +491,8 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
499
491
  wrappedKey: { ciphertext: wrappedKeySealed.ciphertext, nonce: wrappedKeySealed.nonce, algorithm: "AES-256-GCM" },
500
492
  });
501
493
  storeSignEnrollment(mailboxUid!, enrollmentId);
494
+ // Followed from now on (and after this page is left): the server has just accepted it, so it is pending - no need to ask first.
495
+ watchEnrollment(mailboxUid!, enrollmentId, { initial: { status: "pending" } });
502
496
  setSigningEnrollmentId(enrollmentId);
503
497
  setSigningStatus("pending");
504
498
  } catch (err) {
@@ -552,7 +546,8 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
552
546
  await removeMasterKeyWrap(mailboxUid!, wrap.method, wrap.methodId);
553
547
  await loadVault();
554
548
  } catch (err) {
555
- setActionError(err instanceof ApiRequestError ? err.message : "Could not remove this unlock method.");
549
+ // A pop-up (the confirmation is already closed); `actionError` keeps the guidance tied to the forms below.
550
+ notifyApiError(err, "Couldn't remove this unlock method");
556
551
  } finally {
557
552
  setRemovingMethod(null);
558
553
  }
@@ -889,6 +884,27 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
889
884
  );
890
885
  }
891
886
 
887
+ // The "Digital signatures" section: a pending enrollment first (even while an older certificate is still active - it is a renewal); else the
888
+ // active certificate; else how the last enrollment ended (a failure, or a certificate that has since expired); else nothing has been requested.
889
+ const expiredSigningKey = displayedKeys
890
+ .filter((key) => key.useType === "sign" && !key.revokedAt && key.notAfter <= Date.now())
891
+ .sort((a, b) => b.notAfter - a.notAfter)[0];
892
+ // A certificate the CA has issued but the server has not installed yet counts as issued too (there is no key to show yet: the card says it is installing).
893
+ const installing = canManageKeys && isInstalling(enrollmentResult);
894
+ const certificateMode: SigningCertificateMode | null =
895
+ signingStatus === "pending"
896
+ ? "pending"
897
+ : activeSigningKey || installing
898
+ ? canManageKeys
899
+ ? "issued"
900
+ : null
901
+ : canManageKeys && enrollmentResult?.status === "failed"
902
+ ? "failed"
903
+ : canManageKeys && expiredSigningKey
904
+ ? "expired"
905
+ : null;
906
+ const certificateEnrollment = certificateMode === "issued" && enrollmentResult?.status !== "issued" ? null : certificateMode === "expired" ? null : enrollmentResult;
907
+
892
908
  return (
893
909
  <div className="flex-1 min-w-0 overflow-y-auto p-6">
894
910
  <div className="max-w-xl flex flex-col gap-6">
@@ -931,36 +947,55 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
931
947
 
932
948
  <div>
933
949
  <h2 className="text-sm font-semibold mb-2">Digital signatures</h2>
934
- {activeSigningKey ? (
935
- <p className="text-sm text-text-muted">
950
+ {activeSigningKey && signingStatus !== "pending" && (
951
+ <p className={["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" ")}>
936
952
  Enabled — outgoing mail from this mailbox is signed with a publicly-trusted
937
953
  certificate.
938
954
  </p>
939
- ) : signingStatus === "pending" ? (
940
- <p className="text-sm text-text-muted">
941
- Requested — a public certificate authority issues this via an automated email
942
- exchange, which can take a few minutes. You can leave this page; it finishes in
943
- the background and takes effect automatically once issued.
944
- </p>
945
- ) : !canManageKeys ? (
955
+ )}
956
+ {certificateMode && (
957
+ <div className="flex flex-col gap-2">
958
+ {certificateMode === "failed" && signingError && <Alert>{signingError}</Alert>}
959
+ <SigningCertificateCard
960
+ mode={certificateMode}
961
+ enrollment={certificateEnrollment}
962
+ snapshot={enrollment}
963
+ info={signingInfo}
964
+ address={mailbox.primarySmtpAddress}
965
+ keyNotAfter={certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter}
966
+ canRequest={canManageKeys}
967
+ requesting={signingStatus === "enrolling"}
968
+ onCheck={canManageKeys ? () => void checkEnrollmentNow(mailboxUid!) : undefined}
969
+ onRequest={handleEnrollSigning}
970
+ />
971
+ </div>
972
+ )}
973
+ {!activeSigningKey && !certificateMode && !canManageKeys ? (
946
974
  <p className="text-sm text-text-muted">Not enabled.</p>
947
- ) : (
975
+ ) : activeSigningKey || certificateMode ? null : (
948
976
  <div className="flex flex-col gap-2">
949
977
  <p className="text-xs text-text-muted">
950
978
  Lets recipients verify that mail from this mailbox is genuinely from you.
951
979
  Optional — encryption already works without it.
952
980
  </p>
981
+ {beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (
982
+ <p className="text-xs text-text-muted">{beforeRequestText(signingInfo, mailbox.primarySmtpAddress)}</p>
983
+ )}
984
+ {healthWarning(signingInfo) && <Alert>{healthWarning(signingInfo)}</Alert>}
985
+ {enrollment?.gone && <Alert>{STALE_REQUEST_TEXT}</Alert>}
953
986
  {signingError && <Alert>{signingError}</Alert>}
954
- <Button
955
- type="button"
956
- variant="secondary"
957
- className="!w-auto"
958
- loading={signingStatus === "enrolling"}
959
- disabled={signingStatus !== "idle"}
960
- onClick={handleEnrollSigning}
961
- >
962
- Enable digital signatures
963
- </Button>
987
+ {signingInfo?.backend !== "none" && (
988
+ <Button
989
+ type="button"
990
+ variant="secondary"
991
+ className="!w-auto"
992
+ loading={signingStatus === "enrolling"}
993
+ disabled={signingStatus !== "idle"}
994
+ onClick={handleEnrollSigning}
995
+ >
996
+ Enable digital signatures
997
+ </Button>
998
+ )}
964
999
  </div>
965
1000
  )}
966
1001
  </div>
@@ -19,6 +19,7 @@ import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "./_mai
19
19
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
20
20
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
21
21
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
22
+ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
22
23
 
23
24
  const INPUT_CLASS =
24
25
  "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -106,7 +107,8 @@ function MailFilterDetailContent({ uid }: { uid: string }) {
106
107
  setOriginal(updated);
107
108
  setSaved(true);
108
109
  } catch (err) {
109
- setError(err instanceof ApiRequestError ? err.message : "Could not save this mail filter.");
110
+ // A pop-up: `error` is only this form's own validation (and the load failure above).
111
+ notifyApiError(err, "Couldn't save the mail filter");
110
112
  } finally {
111
113
  setSaving(false);
112
114
  }
@@ -18,6 +18,7 @@ import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "../_ma
18
18
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
19
19
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
20
20
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
21
+ import { notifyApiError } from "../../../../shared/notifications/apiErrors.js";
21
22
 
22
23
  const INPUT_CLASS =
23
24
  "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -76,7 +77,8 @@ function NewMailFilterForm() {
76
77
  const created = await createMailFilterRule({ mailboxUid: mailboxUid!, name: name.trim(), ...rule });
77
78
  navigate(`/settings/filters/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`);
78
79
  } catch (err) {
79
- setError(err instanceof ApiRequestError ? err.message : "Could not create the mail filter.");
80
+ // A pop-up: `error` above is only the form's own validation.
81
+ notifyApiError(err, "Couldn't create the mail filter");
80
82
  } finally {
81
83
  setSaving(false);
82
84
  }
@@ -28,6 +28,7 @@ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../sh
28
28
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
29
29
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
30
30
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
31
+ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
31
32
 
32
33
  const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
33
34
 
@@ -125,7 +126,6 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
125
126
  const [loadError, setLoadError] = useState<string | null>(null);
126
127
  const [format, setFormat] = useState<DataExportFormat>("json");
127
128
  const [creating, setCreating] = useState(false);
128
- const [createError, setCreateError] = useState<string | null>(null);
129
129
 
130
130
  // The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before
131
131
  // the initial mount fetch does; without this guard the mount fetch's now-stale response would land
@@ -152,13 +152,12 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
152
152
 
153
153
  async function handleCreate(e: FormEvent) {
154
154
  e.preventDefault();
155
- setCreateError(null);
156
155
  setCreating(true);
157
156
  try {
158
157
  await createExportRequest({ format });
159
158
  await loadRequests();
160
159
  } catch (err) {
161
- setCreateError(err instanceof ApiRequestError ? err.message : "Could not start this export.");
160
+ notifyApiError(err, "Couldn't start the export");
162
161
  } finally {
163
162
  setCreating(false);
164
163
  }
@@ -191,7 +190,6 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
191
190
  </Button>
192
191
  </form>
193
192
 
194
- {createError && <Alert>{createError}</Alert>}
195
193
  {loadError && <Alert>{loadError}</Alert>}
196
194
 
197
195
  {loading ? (
@@ -242,7 +240,6 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
242
240
  const [loading, setLoading] = useState(true);
243
241
  const [loadError, setLoadError] = useState<string | null>(null);
244
242
  const [uploading, setUploading] = useState(false);
245
- const [uploadError, setUploadError] = useState<string | null>(null);
246
243
  const fileInputRef = useRef<HTMLInputElement>(null);
247
244
 
248
245
  // Same stale-response guard as ExportSection's loadRequests() - the Upload button is enabled
@@ -287,11 +284,10 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
287
284
  if (!file || !targetFolderUid) {
288
285
  return;
289
286
  }
290
- setUploadError(null);
291
287
  setUploading(true);
292
288
  uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid })
293
289
  .then(() => loadRequests())
294
- .catch((err) => setUploadError(err instanceof ApiRequestError ? err.message : "Could not upload this file."))
290
+ .catch((err) => notifyApiError(err, "Couldn't upload the file"))
295
291
  .finally(() => setUploading(false));
296
292
  }
297
293
 
@@ -341,7 +337,6 @@ function ImportSection({ mailboxUid }: { mailboxUid?: string }) {
341
337
  />
342
338
 
343
339
  {foldersError && <Alert>{foldersError}</Alert>}
344
- {uploadError && <Alert>{uploadError}</Alert>}
345
340
  {loadError && <Alert>{loadError}</Alert>}
346
341
 
347
342
  {loading ? (
@@ -4,11 +4,10 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { routedPage } from "../../_routedPage.js";
6
6
  import React, { FormEvent, useState } from "react";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
7
  import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
9
8
  import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
10
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
9
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
+ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
12
11
 
13
12
  export type SettingsReadReceiptsPageProps = Omit<SettingsShellProps, "active">;
14
13
 
@@ -34,13 +33,11 @@ function ReadReceiptsContent() {
34
33
  const [autoSendReceiptsExternal, setAutoSendReceiptsExternal] = useState(mailbox.autoSendReceiptsExternal ?? false);
35
34
  // See auto-reply/index.tsx's identical note - later saves must carry the version the previous save returned.
36
35
  const [version, setVersion] = useState(mailbox.version);
37
- const [error, setError] = useState<string | null>(null);
38
36
  const [saving, setSaving] = useState(false);
39
37
  const [saved, setSaved] = useState(false);
40
38
 
41
39
  async function handleSubmit(e: FormEvent) {
42
40
  e.preventDefault();
43
- setError(null);
44
41
  setSaved(false);
45
42
  setSaving(true);
46
43
  try {
@@ -57,7 +54,7 @@ function ReadReceiptsContent() {
57
54
  setVersion(updated.version);
58
55
  setSaved(true);
59
56
  } catch (err) {
60
- setError(err instanceof ApiRequestError ? err.message : "Could not save read receipt settings.");
57
+ notifyApiError(err, "Couldn't save the read receipt settings");
61
58
  } finally {
62
59
  setSaving(false);
63
60
  }
@@ -73,8 +70,7 @@ function ReadReceiptsContent() {
73
70
  checkbox in Compose always overrides the request-side settings below for that one message.
74
71
  </p>
75
72
 
76
- {error && <Alert>{error}</Alert>}
77
- {saved && !error && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
73
+ {saved && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
78
74
 
79
75
  <form onSubmit={handleSubmit} className="flex flex-col gap-5">
80
76
  <fieldset className="flex flex-col gap-2">