@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,116 @@
1
+ /**
2
+ * The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
3
+ * for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
4
+ */
5
+ export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
6
+ /**
7
+ * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
8
+ * Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
9
+ * to just the mail ones itself, or those other apps' folders leak into its sidebar.
10
+ */
11
+ export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
12
+ /** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
13
+ export const SEND_FOLDER_TYPES = ["outbox", "sent_items"];
14
+ export function isMailFolder(folder) {
15
+ return MAIL_FOLDER_TYPES.has(folder.type);
16
+ }
17
+ function sortKey(type) {
18
+ const index = FOLDER_ORDER.indexOf(type);
19
+ return index === -1 ? FOLDER_ORDER.length : index;
20
+ }
21
+ /** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
22
+ export function sortFolders(folders) {
23
+ return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
24
+ }
25
+ /**
26
+ * The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
27
+ * server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
28
+ * arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
29
+ */
30
+ export function folderRows(folders, sending) {
31
+ const rows = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
32
+ if (sending > 0) {
33
+ for (const type of SEND_FOLDER_TYPES) {
34
+ if (!folders.some((folder) => folder.type === type)) {
35
+ rows.push({ kind: "placeholder", type });
36
+ }
37
+ }
38
+ rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
39
+ }
40
+ return rows;
41
+ }
42
+ function rowType(row) {
43
+ return row.kind === "folder" ? row.folder.type : row.type;
44
+ }
45
+ const CHANGEABLE = ["name", "type", "parentFolderUid", "color"];
46
+ /**
47
+ * Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
48
+ * colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
49
+ * nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
50
+ */
51
+ export function upsertFolder(entries, folder) {
52
+ let changed = false;
53
+ const next = entries.map((entry) => {
54
+ const existing = entry.folders.find((f) => f.uid === folder.uid);
55
+ if (!existing) {
56
+ // Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
57
+ if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
58
+ return entry;
59
+ }
60
+ // A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
61
+ // not know it existed) is not a second row: the older of the two is the one shown.
62
+ const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
63
+ if (sameType) {
64
+ if (!isOlder(folder, sameType)) {
65
+ return entry;
66
+ }
67
+ changed = true;
68
+ return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
69
+ }
70
+ changed = true;
71
+ return { ...entry, folders: [...entry.folders, folder] };
72
+ }
73
+ const patch = {};
74
+ for (const key of CHANGEABLE) {
75
+ if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
76
+ patch[key] = folder[key];
77
+ }
78
+ }
79
+ if (Object.keys(patch).length === 0) {
80
+ return entry;
81
+ }
82
+ changed = true;
83
+ const updated = { ...existing, ...patch };
84
+ return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
85
+ });
86
+ return changed ? next : entries;
87
+ }
88
+ function isFullFolder(folder) {
89
+ return typeof folder.name === "string" && typeof folder.type === "string";
90
+ }
91
+ /** Whether `a` was created before `b`: only when both say when. */
92
+ function isOlder(a, b) {
93
+ // (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
94
+ const first = Date.parse(a.dateCreated);
95
+ const second = Date.parse(b.dateCreated);
96
+ return Number.isFinite(first) && Number.isFinite(second) && first < second;
97
+ }
98
+ /** Takes a deleted folder out of the tree. The same array when it was not in it. */
99
+ export function removeFolder(entries, folderUid) {
100
+ if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
101
+ return entries;
102
+ }
103
+ return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
104
+ }
105
+ /**
106
+ * Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
107
+ * are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
108
+ * again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
109
+ */
110
+ export function reconcileFolders(entries, listed) {
111
+ return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
112
+ }
113
+ /** Every folder uid in the tree. */
114
+ export function knownFolderUids(entries) {
115
+ return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
116
+ }
@@ -0,0 +1,46 @@
1
+ import type { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
2
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ /**
4
+ * A short-lived, in-memory copy of a folder's listing, so that switching to a folder that was shown a moment ago (or coming back
5
+ * to Mail from another app) shows its rows on the very frame of the click and revalidates behind them (stale-while-revalidate),
6
+ * instead of replacing the list with a loading state and waiting for the network. It is the client-side router's answer to
7
+ * "folder navigation takes seconds": the list of the folder being switched to is usually already known.
8
+ *
9
+ * Only a plain folder's listing is kept - a search's, an aggregate ("All Mailboxes") view's and a load-more page's rows are not -
10
+ * and what is kept is exactly what was on screen, including the rows "load more" added, the selection and the scroll position,
11
+ * so returning to a folder puts the reader where they left it. A snapshot only makes the first paint fast; the listing is always
12
+ * fetched again and folded in (`mergeFirstPage()`), so nothing shown from here is ever taken as current.
13
+ */
14
+ export interface ListSnapshot {
15
+ messages: Message[];
16
+ conversations: ConversationSummary[];
17
+ hasMore: boolean;
18
+ /** The message that was selected (the reading pane's), if it is still in `messages` when the snapshot is shown. */
19
+ selectedUid: string | null;
20
+ /** The list's `scrollTop`, saved when the listing was left. */
21
+ scrollTop: number;
22
+ /** When it was last written. */
23
+ at: number;
24
+ }
25
+ /** How long a snapshot may be shown after it was last written. Longer than that the folder is shown as loading, as before. */
26
+ export declare const LIST_SNAPSHOT_TTL_MS: number;
27
+ /** How many folders are remembered; the least recently written is dropped first. */
28
+ export declare const LIST_SNAPSHOT_MAX = 16;
29
+ /** Everything that makes one listing different from another: which folder, as messages or conversations, and how it is
30
+ * filtered and sorted. */
31
+ export declare function listSnapshotKey(parts: {
32
+ mailboxUid: string | undefined;
33
+ folderUid: string | undefined;
34
+ conversations: boolean;
35
+ filter: string;
36
+ labels: string;
37
+ sort: string;
38
+ }): string;
39
+ /** The snapshot for `key`, or `undefined` when there is none or it is older than `LIST_SNAPSHOT_TTL_MS`. */
40
+ export declare function readListSnapshot(key: string, now?: number): ListSnapshot | undefined;
41
+ /** Remembers a listing as it is on screen (keeping the scroll position saved for it, if any). */
42
+ export declare function writeListSnapshot(key: string, listing: Omit<ListSnapshot, "scrollTop" | "at">, now?: number): void;
43
+ /** Records how far `key`'s list was scrolled when it was left. A no-op for a listing that was never remembered. */
44
+ export declare function saveListScroll(key: string, scrollTop: number): void;
45
+ /** Forgets every snapshot (sign-out, and tests). */
46
+ export declare function clearListSnapshots(): void;
@@ -0,0 +1,43 @@
1
+ /** How long a snapshot may be shown after it was last written. Longer than that the folder is shown as loading, as before. */
2
+ export const LIST_SNAPSHOT_TTL_MS = 5 * 60000;
3
+ /** How many folders are remembered; the least recently written is dropped first. */
4
+ export const LIST_SNAPSHOT_MAX = 16;
5
+ const snapshots = new Map();
6
+ /** Everything that makes one listing different from another: which folder, as messages or conversations, and how it is
7
+ * filtered and sorted. */
8
+ export function listSnapshotKey(parts) {
9
+ return [parts.mailboxUid, parts.folderUid, parts.conversations ? "conversations" : "messages", parts.filter, parts.labels, parts.sort].join("|");
10
+ }
11
+ /** The snapshot for `key`, or `undefined` when there is none or it is older than `LIST_SNAPSHOT_TTL_MS`. */
12
+ export function readListSnapshot(key, now = Date.now()) {
13
+ const snapshot = snapshots.get(key);
14
+ if (!snapshot) {
15
+ return undefined;
16
+ }
17
+ if (now - snapshot.at > LIST_SNAPSHOT_TTL_MS) {
18
+ snapshots.delete(key);
19
+ return undefined;
20
+ }
21
+ return snapshot;
22
+ }
23
+ /** Remembers a listing as it is on screen (keeping the scroll position saved for it, if any). */
24
+ export function writeListSnapshot(key, listing, now = Date.now()) {
25
+ const previous = snapshots.get(key);
26
+ // Re-inserted so that the Map's order is least-recently-written first.
27
+ snapshots.delete(key);
28
+ snapshots.set(key, { ...listing, scrollTop: previous?.scrollTop ?? 0, at: now });
29
+ while (snapshots.size > LIST_SNAPSHOT_MAX) {
30
+ snapshots.delete(snapshots.keys().next().value);
31
+ }
32
+ }
33
+ /** Records how far `key`'s list was scrolled when it was left. A no-op for a listing that was never remembered. */
34
+ export function saveListScroll(key, scrollTop) {
35
+ const snapshot = snapshots.get(key);
36
+ if (snapshot) {
37
+ snapshot.scrollTop = scrollTop;
38
+ }
39
+ }
40
+ /** Forgets every snapshot (sign-out, and tests). */
41
+ export function clearListSnapshots() {
42
+ snapshots.clear();
43
+ }
@@ -0,0 +1,32 @@
1
+ import { Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { CountTracker } from "./folderCounts.js";
3
+ /**
4
+ * The one way a message's read state changes in the webmail, so that every place that shows it moves together: the row in
5
+ * the list (and the conversation row's unread count), the folder badge, and the unread styling.
6
+ *
7
+ * It is **optimistic**: the row and the badge change the moment the user acts, the server is asked, and if it refuses the
8
+ * change is taken back. What each place is handed to do that is a `ReadStateSink`.
9
+ */
10
+ export interface ReadStateSink {
11
+ /**
12
+ * Shows a message wherever the caller lists it - the message list, the conversation list's child rows, the open thread.
13
+ * `previous` is given with the optimistic copy (and with the revert of it) and is the copy being replaced, so a
14
+ * conversation row can tell that its unread count went up or down; it is absent for the server's own copy, which
15
+ * differs only in its `version`.
16
+ */
17
+ patch(updated: Message, previous?: Message): void;
18
+ /** Applies the change to the folder badges: `MailShell`'s `trackMessageChange`. */
19
+ track(previous: Message, next: Message): CountTracker;
20
+ }
21
+ /**
22
+ * Marks one message read (`read: true`) or unread. Resolves the server's updated copy, or `undefined` when nothing needed
23
+ * doing (it already was that way) or the server refused - in which case everything has already been put back as it was.
24
+ * Never rejects: like the old mark-as-read, a failure must not get in the way of reading the message.
25
+ */
26
+ export declare function setReadState(message: Message, read: boolean, sink: ReadStateSink): Promise<Message | undefined>;
27
+ /**
28
+ * The same for a whole selection, in one request. Resolves the updated messages, in request order. Unlike `setReadState()`
29
+ * it rejects when the server refuses (after putting everything back), because a bulk update is not atomic - some of it may
30
+ * have landed - and the caller's own handling reloads the list and says so (see `bulkUpdateMessages()`).
31
+ */
32
+ export declare function setReadStateMany(messages: Message[], read: boolean, sink: ReadStateSink): Promise<Message[]>;
@@ -0,0 +1,63 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { setMessageRead, setMessagesRead } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ function withRead(message, read) {
7
+ return { ...message, flags: { ...message.flags, read } };
8
+ }
9
+ /** Whether `message` is not yet in the state `read` asks for. A message with no `read` flag counts as unread. */
10
+ function needsChange(message, read) {
11
+ return (message.flags.read === true) !== read;
12
+ }
13
+ /**
14
+ * Marks one message read (`read: true`) or unread. Resolves the server's updated copy, or `undefined` when nothing needed
15
+ * doing (it already was that way) or the server refused - in which case everything has already been put back as it was.
16
+ * Never rejects: like the old mark-as-read, a failure must not get in the way of reading the message.
17
+ */
18
+ export async function setReadState(message, read, sink) {
19
+ if (!needsChange(message, read)) {
20
+ return undefined;
21
+ }
22
+ const optimistic = withRead(message, read);
23
+ sink.patch(optimistic, message);
24
+ const tracker = sink.track(message, optimistic);
25
+ try {
26
+ const updated = await setMessageRead(message, read);
27
+ sink.patch(updated);
28
+ tracker.settle();
29
+ return updated;
30
+ }
31
+ catch {
32
+ sink.patch(message, optimistic);
33
+ tracker.revert();
34
+ return undefined;
35
+ }
36
+ }
37
+ /**
38
+ * The same for a whole selection, in one request. Resolves the updated messages, in request order. Unlike `setReadState()`
39
+ * it rejects when the server refuses (after putting everything back), because a bulk update is not atomic - some of it may
40
+ * have landed - and the caller's own handling reloads the list and says so (see `bulkUpdateMessages()`).
41
+ */
42
+ export async function setReadStateMany(messages, read, sink) {
43
+ const changes = messages.filter((message) => needsChange(message, read)).map((message) => ({ message, optimistic: withRead(message, read) }));
44
+ const trackers = changes.map(({ message, optimistic }) => {
45
+ sink.patch(optimistic, message);
46
+ return sink.track(message, optimistic);
47
+ });
48
+ try {
49
+ const updated = await setMessagesRead(messages, read);
50
+ for (const message of updated) {
51
+ sink.patch(message);
52
+ }
53
+ trackers.forEach((tracker) => tracker.settle());
54
+ return updated;
55
+ }
56
+ catch (err) {
57
+ changes.forEach(({ message, optimistic }, index) => {
58
+ sink.patch(message, optimistic);
59
+ trackers[index].revert();
60
+ });
61
+ throw err;
62
+ }
63
+ }
@@ -0,0 +1,62 @@
1
+ import type { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /**
3
+ * What decides whether a new message is announced, what the announcement says, and the two settings behind it (both per
4
+ * browser, in `localStorage`): whether new-mail pop-ups are on at all, and what the user told the "turn on desktop
5
+ * notifications" offer.
6
+ *
7
+ * The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
8
+ * ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
9
+ */
10
+ /** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
11
+ export declare const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
12
+ /** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
13
+ export declare const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
14
+ /** How much of the body an announcement quotes. */
15
+ export declare const PREVIEW_MAX_LENGTH = 140;
16
+ /** A message received longer ago than this is not "new": mail imported or migrated in bulk arrives as `create` events too. */
17
+ export declare const MAX_NEW_AGE_MS: number;
18
+ /** Whether new-mail pop-ups are on (the default). */
19
+ export declare function getNewMailPopupsEnabled(): boolean;
20
+ export declare function setNewMailPopupsEnabled(enabled: boolean): void;
21
+ /** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
22
+ export declare function getDesktopOfferDismissed(): boolean;
23
+ export declare function setDesktopOfferDismissed(dismissed: boolean): void;
24
+ /** The browser's notification permission, or `"unsupported"` where there is no Notifications API. */
25
+ export type DesktopPermission = NotificationPermission | "unsupported";
26
+ export declare function desktopPermission(): DesktopPermission;
27
+ /** Asks the browser for permission - call it from a click. Resolves the answer; never rejects. */
28
+ export declare function requestDesktopPermission(): Promise<DesktopPermission>;
29
+ /** What an announcement shows. */
30
+ export interface NewMailNotice {
31
+ /** The message's uid: the announcement's identity (and the desktop notification's `tag`). */
32
+ uid: string;
33
+ mailboxUid: string;
34
+ folderUid: string;
35
+ /** The sender's display name, `""` when there is none. */
36
+ senderName: string;
37
+ /** The sender's address - always shown, so a name can't stand in for who really sent it. */
38
+ senderAddress: string;
39
+ subject: string;
40
+ /** About `PREVIEW_MAX_LENGTH` characters of the body as plain text; `Encrypted message` for an encrypted one. */
41
+ preview: string;
42
+ /** Where opening it goes. */
43
+ href: string;
44
+ }
45
+ /** `text` with those characters replaced by spaces, whitespace folded, and cut to `max` characters (with an ellipsis). */
46
+ export declare function cleanPreview(text: string, max?: number): string;
47
+ /** The text of an announcement of `message`. */
48
+ export declare function noticeFor(message: Message): NewMailNotice;
49
+ /** Every address that is this user's own, lowercased - a message from one of them is not new mail worth announcing. */
50
+ export declare function ownAddressesOf(mailboxes: Mailbox[]): Set<string>;
51
+ /**
52
+ * Whether the arrival of `message` is worth announcing: unread mail in an Inbox (not Drafts, Sent Items, Outbox, Deleted Items,
53
+ * Junk Email or quarantine, which are other folders), that Focused Inbox did not put under Other, that its own user did not
54
+ * send, and that is not old (a bulk import).
55
+ */
56
+ export declare function shouldAnnounce(message: Message, context: {
57
+ folders: Folder[];
58
+ ownAddresses: Set<string>;
59
+ now?: number;
60
+ }): boolean;
61
+ /** `Name <address>`, or the address alone - the desktop notification's title. */
62
+ export declare function noticeSender(notice: Pick<NewMailNotice, "senderName" | "senderAddress">): string;
@@ -0,0 +1,138 @@
1
+ import { formatMailAddress, splitMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
2
+ /**
3
+ * What decides whether a new message is announced, what the announcement says, and the two settings behind it (both per
4
+ * browser, in `localStorage`): whether new-mail pop-ups are on at all, and what the user told the "turn on desktop
5
+ * notifications" offer.
6
+ *
7
+ * The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
8
+ * ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
9
+ */
10
+ /** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
11
+ export const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
12
+ /** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
13
+ export const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
14
+ /** How much of the body an announcement quotes. */
15
+ export const PREVIEW_MAX_LENGTH = 140;
16
+ /** A message received longer ago than this is not "new": mail imported or migrated in bulk arrives as `create` events too. */
17
+ export const MAX_NEW_AGE_MS = 6 * 60 * 60 * 1000;
18
+ /** The subject the server stores for an encrypted message (the real one is inside the ciphertext). */
19
+ const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
20
+ function readStorage(key) {
21
+ try {
22
+ return localStorage.getItem(key);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function writeStorage(key, value) {
29
+ try {
30
+ if (value === null) {
31
+ localStorage.removeItem(key);
32
+ }
33
+ else {
34
+ localStorage.setItem(key, value);
35
+ }
36
+ }
37
+ catch {
38
+ // Storage blocked or full: the choice lasts until the page is reloaded, no longer.
39
+ }
40
+ }
41
+ /** Whether new-mail pop-ups are on (the default). */
42
+ export function getNewMailPopupsEnabled() {
43
+ return readStorage(NEW_MAIL_POPUPS_KEY) !== "off";
44
+ }
45
+ export function setNewMailPopupsEnabled(enabled) {
46
+ writeStorage(NEW_MAIL_POPUPS_KEY, enabled ? null : "off");
47
+ }
48
+ /** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
49
+ export function getDesktopOfferDismissed() {
50
+ return readStorage(DESKTOP_OFFER_KEY) !== null;
51
+ }
52
+ export function setDesktopOfferDismissed(dismissed) {
53
+ writeStorage(DESKTOP_OFFER_KEY, dismissed ? "later" : null);
54
+ }
55
+ export function desktopPermission() {
56
+ return typeof Notification === "undefined" ? "unsupported" : Notification.permission;
57
+ }
58
+ /** Asks the browser for permission - call it from a click. Resolves the answer; never rejects. */
59
+ export async function requestDesktopPermission() {
60
+ if (typeof Notification === "undefined") {
61
+ return "unsupported";
62
+ }
63
+ try {
64
+ return await Notification.requestPermission();
65
+ }
66
+ catch {
67
+ return desktopPermission();
68
+ }
69
+ }
70
+ /** Control characters, and the invisible or bidirectional-override characters that can make text read as something else, as code point ranges. */
71
+ const UNWANTED_RANGES = [
72
+ [0x0000, 0x001f],
73
+ [0x007f, 0x009f],
74
+ [0x200b, 0x200f],
75
+ [0x2028, 0x202e],
76
+ [0x2060, 0x2064],
77
+ [0x2066, 0x2069],
78
+ [0xfeff, 0xfeff],
79
+ ];
80
+ const hex = (code) => "\\u" + code.toString(16).padStart(4, "0");
81
+ const UNWANTED = new RegExp("[" + UNWANTED_RANGES.map(([from, to]) => hex(from) + "-" + hex(to)).join("") + "]+", "g");
82
+ /** `text` with those characters replaced by spaces, whitespace folded, and cut to `max` characters (with an ellipsis). */
83
+ export function cleanPreview(text, max = PREVIEW_MAX_LENGTH) {
84
+ const cleaned = text.replace(UNWANTED, " ").replace(/\s+/g, " ").trim();
85
+ return cleaned.length > max ? cleaned.slice(0, max - 1).trimEnd() + String.fromCharCode(0x2026) : cleaned;
86
+ }
87
+ /** The text of an announcement of `message`. */
88
+ export function noticeFor(message) {
89
+ const { name, address } = splitMailAddress(message.from);
90
+ const encrypted = message.encrypted === true;
91
+ const rawSubject = cleanPreview(message.subject ?? "", 200);
92
+ const subject = !rawSubject || rawSubject === ENCRYPTED_SUBJECT_PLACEHOLDER ? (encrypted ? "(encrypted subject)" : "(no subject)") : rawSubject;
93
+ return {
94
+ uid: message.uid,
95
+ mailboxUid: message.mailboxUid,
96
+ folderUid: message.folderUid,
97
+ // `splitMailAddress()` has already taken control and bidirectional-override characters out of both.
98
+ senderName: name ?? "",
99
+ senderAddress: address,
100
+ subject,
101
+ preview: encrypted ? "Encrypted message" : cleanPreview(message.bodyPreview ?? ""),
102
+ href: `/messages/${encodeURIComponent(message.uid)}`,
103
+ };
104
+ }
105
+ /** Every address that is this user's own, lowercased - a message from one of them is not new mail worth announcing. */
106
+ export function ownAddressesOf(mailboxes) {
107
+ const own = new Set();
108
+ for (const mailbox of mailboxes) {
109
+ for (const address of [mailbox.primarySmtpAddress, ...(mailbox.aliasAddresses ?? [])]) {
110
+ if (address) {
111
+ own.add(address.trim().toLowerCase());
112
+ }
113
+ }
114
+ }
115
+ return own;
116
+ }
117
+ /**
118
+ * Whether the arrival of `message` is worth announcing: unread mail in an Inbox (not Drafts, Sent Items, Outbox, Deleted Items,
119
+ * Junk Email or quarantine, which are other folders), that Focused Inbox did not put under Other, that its own user did not
120
+ * send, and that is not old (a bulk import).
121
+ */
122
+ export function shouldAnnounce(message, context) {
123
+ if (context.folders.find((folder) => folder.uid === message.folderUid)?.type !== "inbox") {
124
+ return false;
125
+ }
126
+ if (message.flags.read === true || message.inferenceClassification === "other") {
127
+ return false;
128
+ }
129
+ if (context.ownAddresses.has((message.from?.address ?? "").trim().toLowerCase())) {
130
+ return false;
131
+ }
132
+ const received = Date.parse(message.receivedDate);
133
+ return Number.isNaN(received) || (context.now ?? Date.now()) - received < MAX_NEW_AGE_MS;
134
+ }
135
+ /** `Name <address>`, or the address alone - the desktop notification's title. */
136
+ export function noticeSender(notice) {
137
+ return formatMailAddress({ address: notice.senderAddress, displayName: notice.senderName || undefined });
138
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Lets code with no React context - a pop-up's "Unlock" action - open the app's one unlock prompt (`UnlockPromptProvider`). Renders nothing;
3
+ * mounted once, inside the provider it reaches.
4
+ */
5
+ export default function UnlockBridge(): null;
@@ -0,0 +1,18 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect } from "react";
6
+ import { useUnlockPrompt } from "../../components/layout/UnlockPromptProvider.js";
7
+ import { registerUnlockOpener } from "./composeBridge.js";
8
+ /**
9
+ * Lets code with no React context - a pop-up's "Unlock" action - open the app's one unlock prompt (`UnlockPromptProvider`). Renders nothing;
10
+ * mounted once, inside the provider it reaches.
11
+ */
12
+ export default function UnlockBridge() {
13
+ const { requestUnlock } = useUnlockPrompt();
14
+ useEffect(() => registerUnlockOpener(async (mailboxUid, keys) => {
15
+ await requestUnlock(mailboxUid, keys);
16
+ }), [requestUnlock]);
17
+ return null;
18
+ }
@@ -0,0 +1,34 @@
1
+ import type { Attachment, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ /**
4
+ * A compose window to re-open around a message that was already composed: a failed send's "Open draft". The message keeps its uid - the
5
+ * server draft it was assembled into - and everything the sender had typed comes back exactly as it was, including what could not have
6
+ * been read back from the server (the plain text of a message that was encrypted before it was stored).
7
+ */
8
+ export interface ResumeCompose {
9
+ /** The server draft the window continues - it must be in Drafts (a message that is in Outbox is moved back first). */
10
+ draft: Message;
11
+ mailboxUid: string;
12
+ to: string;
13
+ cc: string;
14
+ bcc: string;
15
+ subject: string;
16
+ html: string;
17
+ attachments: Attachment[];
18
+ requestReceipt: boolean;
19
+ signEnabled: boolean;
20
+ encryptRequested: boolean;
21
+ }
22
+ /** What the app frame's compose provider offers to code that has no React context (a pop-up's action, a push-event handler). */
23
+ export interface ComposeOpenerInput {
24
+ mailboxUid?: string;
25
+ resume?: ResumeCompose;
26
+ }
27
+ /** Registered by `ComposeProvider`; returns the function that unregisters it. */
28
+ export declare function registerComposeOpener(open: (input: ComposeOpenerInput) => void): () => void;
29
+ /** Opens a compose window from outside React. `false` when no compose provider is mounted (nothing was opened). */
30
+ export declare function openComposeFromOutside(input: ComposeOpenerInput): boolean;
31
+ /** Registered by the unlock prompt's owner; returns the function that unregisters it. */
32
+ export declare function registerUnlockOpener(unlock: (mailboxUid: string, keys: PublicKey[]) => Promise<void>): () => void;
33
+ /** Opens the existing unlock prompt from outside React. Resolves once the keys are unlocked; rejects when the user dismissed it (or there is no prompt). */
34
+ export declare function requestUnlockFromOutside(mailboxUid: string, keys: PublicKey[]): Promise<void>;
@@ -0,0 +1,32 @@
1
+ let opener;
2
+ let unlocker;
3
+ /** Registered by `ComposeProvider`; returns the function that unregisters it. */
4
+ export function registerComposeOpener(open) {
5
+ opener = open;
6
+ return () => {
7
+ if (opener === open) {
8
+ opener = undefined;
9
+ }
10
+ };
11
+ }
12
+ /** Opens a compose window from outside React. `false` when no compose provider is mounted (nothing was opened). */
13
+ export function openComposeFromOutside(input) {
14
+ if (!opener) {
15
+ return false;
16
+ }
17
+ opener(input);
18
+ return true;
19
+ }
20
+ /** Registered by the unlock prompt's owner; returns the function that unregisters it. */
21
+ export function registerUnlockOpener(unlock) {
22
+ unlocker = unlock;
23
+ return () => {
24
+ if (unlocker === unlock) {
25
+ unlocker = undefined;
26
+ }
27
+ };
28
+ }
29
+ /** Opens the existing unlock prompt from outside React. Resolves once the keys are unlocked; rejects when the user dismissed it (or there is no prompt). */
30
+ export function requestUnlockFromOutside(mailboxUid, keys) {
31
+ return unlocker ? unlocker(mailboxUid, keys) : Promise.reject(new Error("No unlock prompt is available."));
32
+ }
@@ -0,0 +1,41 @@
1
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /**
3
+ * What a message in Outbox is doing, read from the fields the server keeps on it (`scheduledSend*`):
4
+ *
5
+ * These are the server's own definitions (`@rapidmx/restapi`'s background send):
6
+ *
7
+ * - `sending`: a lease (`scheduledSendLeaseExpiresAt`) is running, or a *due* `scheduledSendTime` with no failed attempts yet - a message
8
+ * queued for the server's worker (a background send is queued as an ordinary due message, `scheduledSendTime` = when it was queued);
9
+ * - `retrying`: `scheduledSendAttempts > 0` with a later `scheduledSendTime` (or a lease on one): a delivery attempt failed and the server
10
+ * will try again;
11
+ * - `failed`: `scheduledSendError` set and no `scheduledSendTime` - the server gave up; it stays in Outbox, waiting for the user to retry
12
+ * (the same send call again) or to open it (back to Drafts is allowed, it is not in flight);
13
+ * - `scheduled`: a send-later message whose time has not come.
14
+ *
15
+ * "Due" is judged against the message's own `dateModified` as well as the clock: a background send's `scheduledSendTime` is set by the server
16
+ * at the moment it queued the message, so a browser whose clock runs behind must not read it as a send scheduled for the future.
17
+ */
18
+ export type OutboxItemState = "sending" | "retrying" | "failed" | "scheduled";
19
+ export interface OutboxItemStatus {
20
+ state: OutboxItemState;
21
+ /** Failed delivery attempts so far (`retrying`). */
22
+ attempts: number;
23
+ /** Why it failed (`failed`). */
24
+ error?: string;
25
+ /** When it will be sent or tried again (`scheduled`, `retrying`), epoch ms. */
26
+ at?: number;
27
+ }
28
+ /** A `scheduledSendTime` this close to the message's last modification was set by the server as "now": a due message, not a scheduled one. */
29
+ export declare const DUE_SLACK_MS = 2000;
30
+ export declare function outboxItemStatus(message: Message, now?: number): OutboxItemStatus;
31
+ /** How many messages of each kind an Outbox folder holds. */
32
+ export interface OutboxFolderStatus {
33
+ sending: number;
34
+ retrying: number;
35
+ failed: number;
36
+ scheduled: number;
37
+ }
38
+ export declare const EMPTY_OUTBOX_STATUS: OutboxFolderStatus;
39
+ export declare function summarizeOutbox(messages: Message[], now?: number): OutboxFolderStatus;
40
+ /** The text a screen reader gets for an Outbox count, and the tooltip: "2 messages sending", "3 messages, 1 failed". */
41
+ export declare function outboxLabel(total: number, status: OutboxFolderStatus | undefined, pendingHere: number): string;