@rapidmx/web-client 0.9.0 → 0.11.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 (443) hide show
  1. package/README.md +373 -88
  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/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  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 +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  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 +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -0,0 +1,251 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { EnrollmentResult, SignEnrollmentStage, SignEnrollmentStep } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
6
+ import type { SigningEnrollmentInfo } from "./signingInfo.js";
7
+
8
+ /** Warn about an expiring certificate this many days ahead. */
9
+ export const EXPIRY_WARNING_DAYS = 30;
10
+
11
+ const DAY_MS = 86_400_000;
12
+
13
+ /** Epoch ms of a date the server sent, or `undefined` for a missing or unparseable one. */
14
+ export function timeOf(value: string | number | undefined): number | undefined {
15
+ if (value === undefined) {
16
+ return undefined;
17
+ }
18
+ const time = typeof value === "number" ? value : Date.parse(value);
19
+ return Number.isFinite(time) ? time : undefined;
20
+ }
21
+
22
+ /** "just now", "20 s ago", "3 min ago", "2 h ago", "4 days ago" - `undefined` when there is no date. A time slightly in the future (clock skew) is "just now". */
23
+ export function relativeTime(value: string | number | undefined, now: number): string | undefined {
24
+ const time = timeOf(value);
25
+ if (time === undefined) {
26
+ return undefined;
27
+ }
28
+ const seconds = Math.max(0, Math.floor((now - time) / 1000));
29
+ if (seconds < 5) {
30
+ return "just now";
31
+ }
32
+ if (seconds < 60) {
33
+ return `${seconds} s ago`;
34
+ }
35
+ const minutes = Math.floor(seconds / 60);
36
+ if (minutes < 60) {
37
+ return `${minutes} min ago`;
38
+ }
39
+ const hours = Math.floor(minutes / 60);
40
+ if (hours < 24) {
41
+ return `${hours} h ago`;
42
+ }
43
+ const days = Math.floor(hours / 24);
44
+ return `${days} ${days === 1 ? "day" : "days"} ago`;
45
+ }
46
+
47
+ /** A calendar date in the reader's language ("21 Sep 2027"); the raw text if it is not a date. */
48
+ export function formatDate(value: string | number | undefined): string {
49
+ const time = timeOf(value);
50
+ if (time === undefined) {
51
+ return typeof value === "string" ? value : "";
52
+ }
53
+ return new Date(time).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
54
+ }
55
+
56
+ /** A date and time for a step's timestamp. */
57
+ export function formatDateTime(value: string | number | undefined): string {
58
+ const time = timeOf(value);
59
+ if (time === undefined) {
60
+ return typeof value === "string" ? value : "";
61
+ }
62
+ return new Date(time).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
63
+ }
64
+
65
+ /** A long serial number as `0A1B2C3D...9F8E7D` (the whole value is what gets copied). */
66
+ export function truncateSerial(serial: string, keep = 8): string {
67
+ return serial.length > keep * 2 + 3 ? `${serial.slice(0, keep)}...${serial.slice(-keep)}` : serial;
68
+ }
69
+
70
+ export type ExpiryState = "ok" | "soon" | "expired" | "unknown";
71
+
72
+ /** How long a certificate has left: `soon` inside `EXPIRY_WARNING_DAYS`, `expired` once past. */
73
+ export function expiryOf(notAfter: string | number | undefined, now: number): { state: ExpiryState; daysLeft?: number } {
74
+ const time = timeOf(notAfter);
75
+ if (time === undefined) {
76
+ return { state: "unknown" };
77
+ }
78
+ if (time <= now) {
79
+ return { state: "expired", daysLeft: 0 };
80
+ }
81
+ const daysLeft = Math.ceil((time - now) / DAY_MS);
82
+ return { state: daysLeft <= EXPIRY_WARNING_DAYS ? "soon" : "ok", daysLeft };
83
+ }
84
+
85
+ /** The order the server's steps normally go in, with the words for a server that names a stage but not the steps. */
86
+ const STAGE_STEPS: { stage: SignEnrollmentStage; label: string }[] = [
87
+ { stage: "submitted", label: "Request sent to the certificate authority" },
88
+ { stage: "awaiting-challenge", label: "Verification e-mail from the CA" },
89
+ { stage: "challenge-answered", label: "Verification answered" },
90
+ { stage: "validating", label: "CA validates the answer" },
91
+ { stage: "issuing", label: "Certificate issued and installed" },
92
+ ];
93
+
94
+ /** What each stage means, for the line under the bar. */
95
+ const STAGE_TEXT: Record<SignEnrollmentStage, string> = {
96
+ submitted: "Request sent - waiting for the certificate authority to answer",
97
+ "awaiting-challenge": "Waiting for the CA's verification e-mail",
98
+ "challenge-answered": "Verification answered - waiting for the certificate",
99
+ validating: "The CA is validating the answer",
100
+ issuing: "The certificate is being issued",
101
+ issued: "Issued",
102
+ failed: "Failed",
103
+ };
104
+
105
+ /**
106
+ * The steps to draw: the server's own `stages` when it sent them, else - for a server that only says which `stage` it is at - the usual five
107
+ * with everything before it done and it active, else none (an older server: the card falls back to a plain sentence).
108
+ */
109
+ export function stepsOf(result: EnrollmentResult | null | undefined): SignEnrollmentStep[] {
110
+ if (!result) {
111
+ return [];
112
+ }
113
+ if (result.stages && result.stages.length > 0) {
114
+ return result.stages;
115
+ }
116
+ if (!result.stage) {
117
+ return [];
118
+ }
119
+ const index = STAGE_STEPS.findIndex((step) => step.stage === result.stage);
120
+ const failed = result.stage === "failed" || result.status === "failed";
121
+ const issued = result.stage === "issued" || result.status === "issued";
122
+ // An issued enrollment has done every step; a failed one stopped at an unknown step, so none is claimed done or active.
123
+ return STAGE_STEPS.map((step, position): SignEnrollmentStep => {
124
+ if (issued) {
125
+ return { id: step.stage, label: step.label, state: "done" };
126
+ }
127
+ if (failed || index === -1) {
128
+ return { id: step.stage, label: step.label, state: "pending" };
129
+ }
130
+ return { id: step.stage, label: step.label, state: position < index ? "done" : position === index ? "active" : "pending" };
131
+ });
132
+ }
133
+
134
+ /** The percentage the bar shows: the server's `progress`, else what the steps say (done steps out of all), else `undefined` - unknown. */
135
+ export function progressOf(result: EnrollmentResult | null | undefined): number | undefined {
136
+ if (!result) {
137
+ return undefined;
138
+ }
139
+ if (typeof result.progress === "number") {
140
+ return Math.round(result.progress);
141
+ }
142
+ if (result.status === "issued") {
143
+ return 100;
144
+ }
145
+ const steps = stepsOf(result);
146
+ return steps.length > 0 ? Math.round((steps.filter((step) => step.state === "done").length / steps.length) * 100) : undefined;
147
+ }
148
+
149
+ /** The step doing the work now, if any. */
150
+ export function activeStepOf(result: EnrollmentResult | null | undefined): SignEnrollmentStep | undefined {
151
+ return stepsOf(result).find((step) => step.state === "active");
152
+ }
153
+
154
+ /**
155
+ * A certificate that is issued but not in the mailbox yet: the server installs it with a job a few minutes after the CA issues it, and says when it
156
+ * has (`installedAt`). An older server sends neither date, so its issued certificates are never "installing".
157
+ */
158
+ export function isInstalling(result: EnrollmentResult | null | undefined): boolean {
159
+ return result?.status === "issued" && !!result.issuedAt && !result.installedAt;
160
+ }
161
+
162
+ /** What the server says about a CA it could not reach: still pending, to be retried - not a failure. */
163
+ export const CA_UNREACHABLE_TEXT = "The certificate authority could not be reached just now - it will be asked again.";
164
+
165
+ /** The plain-language line: what the enrollment is waiting for, when it was issued, why it failed. `undefined` when the server said nothing to go on. */
166
+ export function statusLineOf(result: EnrollmentResult | null | undefined): string | undefined {
167
+ if (!result) {
168
+ return undefined;
169
+ }
170
+ if (result.status === "issued") {
171
+ if (isInstalling(result)) {
172
+ return "Issued - installing it on your mailbox (this takes a few minutes)";
173
+ }
174
+ return result.notAfter ? `Issued - valid until ${formatDate(result.notAfter)}` : "Issued";
175
+ }
176
+ if (result.status === "failed") {
177
+ return `Failed: ${result.error ?? "the certificate authority did not issue a certificate."}`;
178
+ }
179
+ return result.stage ? STAGE_TEXT[result.stage] : undefined;
180
+ }
181
+
182
+ /** What a pending request says when the server has not said how it issues certificates (an older server): nothing that is only true of one way of issuing them. */
183
+ export const PENDING_FALLBACK_TEXT = "Requested - waiting for the certificate. Use Check status to see where the request is.";
184
+
185
+ /** A manual deployment: nobody but an administrator can make the certificate. Never "takes effect automatically". */
186
+ export const MANUAL_ISSUE_TEXT = "This server issues signing certificates manually: an administrator has to upload the certificate for your request. Contact your administrator.";
187
+
188
+ /** The enrollment id the browser kept is one the server does not know any more (another provider after a configuration change, a cancelled request). */
189
+ export const STALE_REQUEST_TEXT = "This request is no longer active - request a new certificate.";
190
+
191
+ /** How a request is being turned into a certificate: by a public CA on its own, by an administrator, or not known. The enrollment's own `provider` wins over what the deployment says now. */
192
+ export type IssueMode = "automatic" | "manual" | "unknown";
193
+
194
+ export function issueModeOf(result: EnrollmentResult | null | undefined, info: SigningEnrollmentInfo | null | undefined): IssueMode {
195
+ const provider = result?.provider;
196
+ if (provider === "manual" || (!provider && info?.backend === "manual")) {
197
+ return "manual";
198
+ }
199
+ if (provider === "rfc8823" || (!provider && info?.automatic)) {
200
+ return "automatic";
201
+ }
202
+ return "unknown";
203
+ }
204
+
205
+ function minutes(count: number): string {
206
+ return `${count} ${count === 1 ? "minute" : "minutes"}`;
207
+ }
208
+
209
+ /** "Requested from ca.example. The CA sends a verification e-mail to jane@example.com; this usually takes about 5 minutes." - each part only as far as it is known. */
210
+ export function requestedFromText(info: SigningEnrollmentInfo | null | undefined, address: string | undefined): string {
211
+ const host = info?.ca?.host ? `Requested from ${info.ca.host}.` : "Requested from the certificate authority.";
212
+ const sentence = `The CA sends a verification e-mail to ${address ?? "this mailbox"}`;
213
+ const duration = info?.typicalDurationMinutes ? `; this usually takes about ${minutes(info.typicalDurationMinutes)}` : "";
214
+ return `${host} ${sentence}${duration}.`;
215
+ }
216
+
217
+ /** What to say before anyone has asked for a certificate: how this deployment will issue it (or that it does not). `undefined` when nothing is known. */
218
+ export function beforeRequestText(info: SigningEnrollmentInfo | null | undefined, address: string | undefined): string | undefined {
219
+ if (!info || info.backend === "none") {
220
+ return info ? "This server does not issue signing certificates." : undefined;
221
+ }
222
+ if (info.backend === "manual") {
223
+ return MANUAL_ISSUE_TEXT;
224
+ }
225
+ const from = info.ca?.host ?? "a public certificate authority";
226
+ const duration = info.typicalDurationMinutes ? `, usually within about ${minutes(info.typicalDurationMinutes)}` : "";
227
+ return `A certificate is issued automatically by ${from}: it sends a verification e-mail to ${address ?? "this mailbox"}${duration}.`;
228
+ }
229
+
230
+ /** The warning when the CA has been reporting a problem: what it said and when it last worked. `undefined` when it has not. */
231
+ export function healthWarning(info: SigningEnrollmentInfo | null | undefined): string | undefined {
232
+ const error = info?.health?.lastError;
233
+ if (!error) {
234
+ return undefined;
235
+ }
236
+ const last = info.health!.lastSuccessAt ? formatDateTime(info.health!.lastSuccessAt) : "never";
237
+ return `The certificate authority reported a problem: ${error} (last successful contact ${last})`;
238
+ }
239
+
240
+ /** A pending request is "taking longer than expected" after this long without a change. */
241
+ export const OVERDUE_AFTER_MS = 60 * 60_000;
242
+
243
+ /** Whether a pending request was made more than an hour ago and nothing has changed in the last hour: say so instead of staying silent. */
244
+ export function isOverdue(result: EnrollmentResult | null | undefined, now: number): boolean {
245
+ if (result?.status !== "pending") {
246
+ return false;
247
+ }
248
+ const requested = timeOf(result.requestedAt);
249
+ const changed = timeOf(result.updatedAt) ?? requested;
250
+ return requested !== undefined && changed !== undefined && now - requested > OVERDUE_AFTER_MS && now - changed > OVERDUE_AFTER_MS;
251
+ }
@@ -0,0 +1,58 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+ import { SigningEnrollmentInfo, getSigningEnrollmentInfo } from "@rapidmx/react-shared/crypto/signingProviderApi.js";
7
+
8
+ // The type and the request both come from react-shared's `signingProviderApi.js` (R6's) - this module only adds the caching, the
9
+ // never-rejects contract and the hook that Settings > Encryption and its card want. Re-exported so the rest of `signing/` and
10
+ // `SigningCertificateCard` keep importing `SigningEnrollmentInfo` from here rather than reaching into react-shared directly.
11
+ export type { SigningEnrollmentInfo };
12
+
13
+ /** How long an answer is reused: the page asks once when it opens, and again after this if it is opened again. */
14
+ export const SIGNING_INFO_CACHE_MS = 5 * 60_000;
15
+
16
+ let cached: { at: number; promise: Promise<SigningEnrollmentInfo | null> } | undefined;
17
+
18
+ /** The deployment's signing-enrollment description, or `null` when it cannot be had (an older server with no such endpoint, offline,
19
+ * anything) - never rejects. One request at a time, reused for five minutes. */
20
+ export function fetchSigningEnrollmentInfo(now: number = Date.now()): Promise<SigningEnrollmentInfo | null> {
21
+ if (cached && now - cached.at < SIGNING_INFO_CACHE_MS) {
22
+ return cached.promise;
23
+ }
24
+ const promise = getSigningEnrollmentInfo().catch(() => null);
25
+ cached = { at: now, promise };
26
+ // An answer that could not be had is not worth remembering: the next visit asks again.
27
+ void promise.then((info) => {
28
+ if (info === null && cached?.promise === promise) {
29
+ cached = undefined;
30
+ }
31
+ });
32
+ return promise;
33
+ }
34
+
35
+ /** Forgets the remembered answer: for tests, which share the module. */
36
+ export function resetSigningInfo(): void {
37
+ cached = undefined;
38
+ }
39
+
40
+ /** The deployment's description, once it is known: `undefined` while it is being asked for, `null` when there is none. Asks only while `enabled`. */
41
+ export function useSigningEnrollmentInfo(enabled: boolean): SigningEnrollmentInfo | null | undefined {
42
+ const [info, setInfo] = useState<SigningEnrollmentInfo | null | undefined>(undefined);
43
+ useEffect(() => {
44
+ if (!enabled) {
45
+ return;
46
+ }
47
+ let cancelled = false;
48
+ void fetchSigningEnrollmentInfo().then((answer) => {
49
+ if (!cancelled) {
50
+ setInfo(answer);
51
+ }
52
+ });
53
+ return () => {
54
+ cancelled = true;
55
+ };
56
+ }, [enabled]);
57
+ return info;
58
+ }
@@ -0,0 +1,19 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+
7
+ /** The current time in epoch ms, re-read every `intervalMs` while `enabled` - for "3 min ago" texts and countdowns that keep themselves right. */
8
+ export function useNow(intervalMs: number, enabled = true): number {
9
+ const [now, setNow] = useState(() => Date.now());
10
+ useEffect(() => {
11
+ if (!enabled) {
12
+ return;
13
+ }
14
+ setNow(Date.now());
15
+ const timer = setInterval(() => setNow(Date.now()), intervalMs);
16
+ return () => clearInterval(timer);
17
+ }, [intervalMs, enabled]);
18
+ return now;
19
+ }
@@ -0,0 +1,90 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useRef } from "react";
6
+ import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { getCurrentSignEnrollment } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
8
+ import { notify } from "../notifications/store.js";
9
+ import { readStoredSignEnrollment, storeSignEnrollment } from "./enrollmentStorage.js";
10
+ import { EnrollmentSnapshot, onEnrollmentEnded, watchCurrentEnrollment, watchEnrollment } from "./enrollmentTracker.js";
11
+ import { formatDate, isInstalling } from "./enrollmentView.js";
12
+
13
+ /** Where the pop-up's action leads: the page that shows the certificate. */
14
+ export const SIGNING_SETTINGS_HREF = "/settings/encryption";
15
+
16
+ /**
17
+ * Tells the user, wherever they are in the app, when a signing certificate they asked for is issued or has failed. Lives in the persistent app
18
+ * frame (`AppChrome`): for each mailbox the user owns it follows the enrollment this browser has a stored id for and - for one started on another
19
+ * device - the one the server names as the mailbox's current enrollment when it is still pending (`GET .../sign-enrollment`; an older server
20
+ * answers 404 and nothing is followed). Reading, backoff and visibility rules are the tracker's (`enrollmentTracker.ts`), shared with the
21
+ * Settings > Encryption card, so an enrollment is asked about once. The pop-up is raised once per enrollment (`dedupeKey`) and only for one that
22
+ * was seen pending - never for a certificate issued long ago.
23
+ */
24
+ export function useSigningEnrollmentWatcher({ userUid, mailboxes, enabled }: { userUid?: string; mailboxes: Mailbox[]; enabled: boolean }): void {
25
+ const mailboxesRef = useRef(mailboxes);
26
+ mailboxesRef.current = mailboxes;
27
+ // Keyed on the uids of the mailboxes the user owns: a refreshed list of the same mailboxes must not start over.
28
+ const ownedKey = mailboxes
29
+ .filter((mailbox) => mailbox.ownerUserUid === userUid)
30
+ .map((mailbox) => mailbox.uid)
31
+ .join("|");
32
+
33
+ useEffect(() => {
34
+ if (!enabled || !userUid || !ownedKey) {
35
+ return;
36
+ }
37
+ let cancelled = false;
38
+ const releases: (() => void)[] = [];
39
+ for (const mailboxUid of ownedKey.split("|")) {
40
+ const storedId = readStoredSignEnrollment(mailboxUid);
41
+ if (storedId) {
42
+ releases.push(watchEnrollment(mailboxUid, storedId));
43
+ continue;
44
+ }
45
+ // Never throws or rejects: a lookup that cannot be made (an older server, offline, anything) is "nothing is pending", and the frame goes on.
46
+ void (async () => {
47
+ try {
48
+ const current = await getCurrentSignEnrollment(mailboxUid);
49
+ if (!cancelled && current?.status === "pending") {
50
+ storeSignEnrollment(mailboxUid, current.enrollmentId);
51
+ releases.push(watchCurrentEnrollment(mailboxUid, current));
52
+ }
53
+ } catch {
54
+ // Nothing to follow.
55
+ }
56
+ })();
57
+ }
58
+ const stopListening = onEnrollmentEnded((snapshot) => announce(snapshot, mailboxesRef.current));
59
+ return () => {
60
+ cancelled = true;
61
+ stopListening();
62
+ releases.forEach((release) => release());
63
+ };
64
+ }, [enabled, userUid, ownedKey]);
65
+ }
66
+
67
+ /** The pop-up for an enrollment that just ended - issued or failed (the tracker announces nothing else, and always with its answer). */
68
+ function announce(snapshot: EnrollmentSnapshot, mailboxes: Mailbox[]): void {
69
+ const address = mailboxes.find((mailbox) => mailbox.uid === snapshot.mailboxUid)?.primarySmtpAddress;
70
+ const result = snapshot.result!;
71
+ if (result.status === "issued") {
72
+ notify({
73
+ kind: "success",
74
+ title: "Digital signature certificate issued",
75
+ message: isInstalling(result)
76
+ ? `The certificate for ${address ?? "your mailbox"} is issued and is being installed - mail is signed once it is (a few minutes).`
77
+ : `${address ? `Mail from ${address}` : "Your mail"} is now signed${result.notAfter ? ` (valid until ${formatDate(result.notAfter)})` : ""}.`,
78
+ actions: [{ label: "View", href: SIGNING_SETTINGS_HREF }],
79
+ dedupeKey: `sign-enrollment:${snapshot.enrollmentId}`,
80
+ });
81
+ } else {
82
+ notify({
83
+ kind: "error",
84
+ title: "Digital signature certificate could not be issued",
85
+ message: result.error ?? "The certificate authority did not issue a certificate.",
86
+ actions: [{ label: result.retryable === false ? "Details" : "Try again", href: SIGNING_SETTINGS_HREF }],
87
+ dedupeKey: `sign-enrollment:${snapshot.enrollmentId}`,
88
+ });
89
+ }
90
+ }
@@ -276,13 +276,111 @@ h5,
276
276
  h6 {
277
277
  font-family: var(--rr-font-display);
278
278
  }
279
-
280
- /* The compose editor's quoted original (`composeQuoting.ts`'s blockquote). TipTap keeps the element but not the
281
- * inline style it was seeded with, and Tailwind's preflight resets the browser's own blockquote indent - without
282
- * this the quote under a reply reads as if it were part of the new message. */
283
- .tiptap blockquote {
284
- margin: 0;
285
- padding-left: 1em;
286
- border-left: 2px solid var(--rr-color-border);
287
- color: var(--rr-color-text-muted);
288
- }
279
+
280
+ /* The compose editor's quoted original (`composeQuoting.ts`'s blockquote). TipTap keeps the element but not the
281
+ * inline style it was seeded with, and Tailwind's preflight resets the browser's own blockquote indent - without
282
+ * this the quote under a reply reads as if it were part of the new message. */
283
+ .tiptap blockquote {
284
+ margin: 0;
285
+ padding-left: 1em;
286
+ border-left: 2px solid var(--rr-color-border);
287
+ color: var(--rr-color-text-muted);
288
+ }
289
+
290
+ /* A new-mail pop-up (`NewMailToasts.tsx`) eases in from just above its place. Only for readers who have not asked their system
291
+ * for less motion: with `prefers-reduced-motion: reduce` it simply appears. */
292
+ @media (prefers-reduced-motion: no-preference) {
293
+ .rr-toast-in {
294
+ animation: rr-toast-in 0.18s ease-out;
295
+ }
296
+ }
297
+ @keyframes rr-toast-in {
298
+ from {
299
+ opacity: 0;
300
+ transform: translateY(-0.5rem);
301
+ }
302
+ to {
303
+ opacity: 1;
304
+ transform: none;
305
+ }
306
+ }
307
+
308
+ /* The frame's own background: the page's `surface-alt`, unless the user has chosen a background (`appearance/theme.ts` then sets `--rr-frame-bg`
309
+ * to `transparent` so the fixed background layer - `html::before`/`html::after` - shows through instead). */
310
+ .rr-frame-bg {
311
+ background-color: var(--rr-frame-bg, var(--rr-color-surface-alt));
312
+ }
313
+
314
+ /* With a background set, `--color-surface` and `--color-surface-alt` (what `bg-surface` and `bg-surface-alt` read) are translucent panels
315
+ * (`appearance/theme.ts`). Anything that must stay readable on its own - menus, dialogs, drawers, list pop-ups and the pop-ups - takes the
316
+ * opaque colours back for itself and everything inside it, and so does anything that stays put while a page scrolls under it (`.rr-solid`: the
317
+ * sticky title bar, the branding header, the phone's bottom tab bar), or the text beneath would show through it. Without a background these two declarations change nothing. */
318
+ [role="menu"],
319
+ [role="dialog"],
320
+ [role="listbox"],
321
+ .rr-toast-in,
322
+ .rr-solid,
323
+ [data-branding-header],
324
+ nav[aria-label="Mobile navigation"] {
325
+ --color-surface: var(--rr-color-surface);
326
+ --color-surface-alt: var(--rr-color-surface-alt);
327
+ }
328
+
329
+ /* The slot a branding header or footer's `{USER_MENU}` becomes (`BrandingChrome.tsx`): the account menu's button sits in the line of text
330
+ * the variable was written in. */
331
+ [data-rr-slot="user-menu"] {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ vertical-align: middle;
335
+ }
336
+
337
+ /* The Outbox pill's "something is on its way" dot (`OutboxBadge`). Still when the user asks for less motion. */
338
+ @media (prefers-reduced-motion: no-preference) {
339
+ .rr-sending-dot {
340
+ animation: rr-sending-dot 1.1s ease-in-out infinite;
341
+ }
342
+ }
343
+ @keyframes rr-sending-dot {
344
+ 0%,
345
+ 100% {
346
+ opacity: 0.35;
347
+ transform: scale(0.8);
348
+ }
349
+ 50% {
350
+ opacity: 1;
351
+ transform: scale(1.15);
352
+ }
353
+ }
354
+
355
+ /* The signing-certificate card's progress bar (`SigningCertificateCard`): a soft highlight sweeping across the filled part while a step is active, and
356
+ * an endless slide for a bar with no number. Both stand still when the user asks for less motion (the indeterminate bar then shows as a faded full bar). */
357
+ .rr-progress-shimmer {
358
+ position: relative;
359
+ overflow: hidden;
360
+ }
361
+ @media (prefers-reduced-motion: no-preference) {
362
+ .rr-progress-shimmer::after {
363
+ content: "";
364
+ position: absolute;
365
+ inset: 0;
366
+ background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.45), transparent);
367
+ transform: translateX(-100%);
368
+ animation: rr-progress-shimmer 1.6s ease-in-out infinite;
369
+ }
370
+ .rr-progress-indeterminate {
371
+ animation: rr-progress-slide 1.4s ease-in-out infinite;
372
+ }
373
+ }
374
+ @keyframes rr-progress-shimmer {
375
+ to {
376
+ transform: translateX(100%);
377
+ }
378
+ }
379
+ @keyframes rr-progress-slide {
380
+ 0% {
381
+ transform: translateX(-100%);
382
+ }
383
+ 100% {
384
+ transform: translateX(300%);
385
+ }
386
+ }
@@ -4,6 +4,7 @@
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { PropsWithChildren } from "react";
6
6
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
7
+ import { AppearanceHead, appearanceHtmlAttributes } from "../shared/appearance/AppearanceHead.js";
7
8
  import { CUSTOM_STYLESHEET_LINK_ID } from "@rapidmx/react-shared/branding/useBranding.js";
8
9
 
9
10
  export interface LayoutProps {
@@ -16,15 +17,19 @@ export interface LayoutProps {
16
17
  * which doesn't receive props at all - falling back to the stock defaults there is acceptable.
17
18
  */
18
19
  branding?: Branding;
20
+ /** The signed-in user's stored appearance preferences (a page prop from the same `fetchProps()`, present only on the www pages and only when the user has any): rendered as a stylesheet in the `<head>` so the very first paint is themed - see `AppearanceHead`. */
21
+ appearance?: unknown;
22
+ /** The signed-in user (supplied by the framework), so a cached appearance from another account on this browser is never applied. */
23
+ userUid?: string;
19
24
  }
20
25
 
21
- export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>) {
26
+ export default function Layout({ children, branding, appearance, userUid }: PropsWithChildren<LayoutProps>) {
22
27
  const title = branding?.title || branding?.companyName ? `${branding?.title || branding?.companyName}: Mail` : "RapidMX: Mail";
23
28
  const iconHref = branding?.iconUrl || branding?.logoUrl || "/images/logo.svg";
24
29
  const stylesheetHref = branding?.stylesheetUrl;
25
30
 
26
31
  return (
27
- <html lang="en">
32
+ <html lang="en" {...appearanceHtmlAttributes(appearance, userUid)}>
28
33
  <head>
29
34
  <meta charSet="utf-8" />
30
35
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -38,6 +43,7 @@ export default function Layout({ children, branding }: PropsWithChildren<LayoutP
38
43
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
39
44
  />
40
45
  {stylesheetHref && <link rel="stylesheet" href={stylesheetHref} id={CUSTOM_STYLESHEET_LINK_ID} />}
46
+ <AppearanceHead appearance={appearance} />
41
47
  </head>
42
48
  <body>{children}</body>
43
49
  </html>
@@ -0,0 +1,24 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { ComponentType } from "react";
6
+ import AppRouter, { RoutedPageComponent } from "../shared/navigation/AppRouter.js";
7
+ import { wwwRoutes } from "./_routes.js";
8
+
9
+ /**
10
+ * Makes a `www` page the entry of the client-side router: the page's default export is `routedPage("/its/route", Page)`.
11
+ * `@rapidrest/react` server-renders and hydrates exactly that component, so what it renders - the router, with the persistent
12
+ * app frame (`AppChrome`) and `Page` inside it - is the whole client tree, and later pages replace `Page` without a page load.
13
+ * `path` is the page's route template, the same as its entry in `_routes.ts`.
14
+ *
15
+ * The result carries the plain `Page` as `.page`, which is what the router shows when it loads this module for a navigation
16
+ * (rendering the wrapped default export there would nest a second router).
17
+ */
18
+ export function routedPage<P extends object>(path: string, Page: ComponentType<P>): RoutedPageComponent<P> {
19
+ function RoutedPage(props: P) {
20
+ return <AppRouter routes={wwwRoutes} initialPath={path} initialPage={Page} pageProps={props} />;
21
+ }
22
+ RoutedPage.page = Page;
23
+ return RoutedPage;
24
+ }
@@ -0,0 +1,36 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { RouteDefinition } from "../shared/navigation/routes.js";
6
+
7
+ /**
8
+ * Every `apps/www` page the client-side router can show without a page load (see `apps/shared/navigation/AppRouter.tsx`), in
9
+ * the order they are matched: literal routes before the parameterized ones that would also match them. Each page loads with a
10
+ * dynamic `import()`, which is what makes it a chunk of its own - only the page a document was rendered for is in its first
11
+ * JavaScript. Keep it in step with the files under `apps/www`; a page missing from here still works, it is just reached with a
12
+ * page load. (The file starts with `_` so `@rapidrest/react` doesn't treat it as a page.)
13
+ *
14
+ * The four app-rail pages are `idlePrefetch`ed: downloaded when the browser is idle after the first page has loaded.
15
+ */
16
+ export const wwwRoutes: readonly RouteDefinition[] = [
17
+ { path: "/", active: "mail", load: () => import("./index.js"), idlePrefetch: true },
18
+ { path: "/messages/:uid", active: "mail", load: () => import("./messages/[uid].js") },
19
+ { path: "/calendar", active: "calendar", load: () => import("./calendar/index.js"), idlePrefetch: true },
20
+ { path: "/contacts", active: "contacts", load: () => import("./contacts/index.js"), idlePrefetch: true },
21
+ { path: "/contacts/:uid", active: "contacts", load: () => import("./contacts/[uid].js") },
22
+ { path: "/tasks", active: "tasks", load: () => import("./tasks/index.js"), idlePrefetch: true },
23
+ { path: "/settings/appearance", active: "settings", load: () => import("./settings/appearance/index.js") },
24
+ { path: "/settings/auto-reply", active: "settings", load: () => import("./settings/auto-reply/index.js") },
25
+ { path: "/settings/encryption", active: "settings", load: () => import("./settings/encryption/index.js") },
26
+ { path: "/settings/filters", active: "settings", load: () => import("./settings/filters/index.js") },
27
+ { path: "/settings/filters/new", active: "settings", load: () => import("./settings/filters/new/index.js") },
28
+ { path: "/settings/filters/:uid", active: "settings", load: () => import("./settings/filters/[uid].js") },
29
+ { path: "/settings/labels", active: "settings", load: () => import("./settings/labels/index.js") },
30
+ { path: "/settings/privacy", active: "settings", load: () => import("./settings/privacy/index.js") },
31
+ { path: "/settings/read-receipts", active: "settings", load: () => import("./settings/read-receipts/index.js") },
32
+ { path: "/settings/sharing", active: "settings", load: () => import("./settings/sharing/index.js") },
33
+ { path: "/settings/signatures", active: "settings", load: () => import("./settings/signatures/index.js") },
34
+ { path: "/settings/signatures/new", active: "settings", load: () => import("./settings/signatures/new/index.js") },
35
+ { path: "/settings/signatures/:uid", active: "settings", load: () => import("./settings/signatures/[uid].js") },
36
+ ];