@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,138 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { createContext, useCallback, useEffect, useRef, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import { FOLDER_ORDER, MAIL_FOLDER_TYPES, isMailFolder, knownFolderUids, reconcileFolders, removeFolder, upsertFolder } from "./folderTree.js";
9
+ import { useMailLiveUpdates } from "./useMailLiveUpdates.js";
10
+ import { useNewMailNotifications } from "./useNewMailNotifications.js";
11
+ import { usePushConnectionNotice } from "../notifications/pushStatus.js";
12
+ import { registerOutboxCounter } from "./outbox/sendJob.js";
13
+ import { handleSendEvent } from "./outbox/sendOutcomes.js";
14
+ import { useOutboxStatus } from "./outbox/useOutboxStatus.js";
15
+ /** Page size of the one `listMailboxes()` call. */
16
+ export const MAILBOX_LIST_LIMIT = 100;
17
+ // The sidebar's order and the folders Mail lists live in `folderTree.ts`; re-exported for what already imports them from here.
18
+ export { FOLDER_ORDER, MAIL_FOLDER_TYPES };
19
+ const NO_TRACKER = { settle: () => undefined, revert: () => undefined };
20
+ /** What the persistent app frame (`AppChrome`) offers to the Mail shell so that it doesn't open a second connection. `null` outside a frame. */
21
+ export const MailConnectionContext = createContext(null);
22
+ /**
23
+ * The user's mailboxes and their mail folders, plus the live connection that keeps them - and the badges, the list, the new-mail
24
+ * pop-ups - current. It is everything `MailShell` used to do on mount, gathered so that it can live in the persistent app frame instead: the
25
+ * push socket (the server allows ten per user), the safety-net poll, the folder counters and the pop-ups then belong to the signed-in session
26
+ * rather than to whichever page happens to be showing, and keep working in Calendar, Contacts, Tasks and Settings. `MailShell` reads the same
27
+ * state from `MailConnectionContext` when there is a frame, and runs this hook itself when there isn't (a page rendered outside the router).
28
+ */
29
+ export function useMailConnection({ userUid, enabled, open }) {
30
+ const [status, setStatus] = useState("checking");
31
+ const [error, setError] = useState(null);
32
+ const [mailboxes, setMailboxes] = useState([]);
33
+ const [mailboxFolders, setMailboxFolders] = useState([]);
34
+ const [foldersLoading, setFoldersLoading] = useState(true);
35
+ useEffect(() => {
36
+ if (!userUid || !enabled) {
37
+ return;
38
+ }
39
+ listMailboxes({ limit: MAILBOX_LIST_LIMIT })
40
+ .then((result) => {
41
+ setMailboxes(result);
42
+ setStatus("ready");
43
+ })
44
+ .catch((err) => {
45
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
46
+ setStatus("error");
47
+ });
48
+ }, [userUid, enabled]);
49
+ // Fans out one listFolders() call per accessible mailbox in parallel - each call catches its own failure into a MailboxFolders.error
50
+ // rather than letting Promise.all reject, so one mailbox's fetch failure renders that section's own inline Alert instead of blanking out
51
+ // every other mailbox's folder tree.
52
+ useEffect(() => {
53
+ if (mailboxes.length === 0) {
54
+ // Deliberately leaves foldersLoading as-is: this also runs once before listMailboxes() has resolved, and clearing it here would
55
+ // briefly render an empty sidebar the moment mailboxes arrive, before their folders do. A genuinely mailbox-less caller gets
56
+ // MailboxProvisioning.
57
+ setMailboxFolders([]);
58
+ return;
59
+ }
60
+ setFoldersLoading(true);
61
+ void Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
62
+ .then((result) => ({ mailbox, folders: result.filter(isMailFolder) }))
63
+ .catch((err) => ({
64
+ mailbox,
65
+ folders: [],
66
+ error: err instanceof ApiRequestError ? err.message : "Could not load folders.",
67
+ }))))
68
+ .then(setMailboxFolders)
69
+ .finally(() => setFoldersLoading(false));
70
+ }, [mailboxes]);
71
+ /** Files a newly created folder under its own mailbox, leaving every other mailbox's list untouched - and ignoring a type the
72
+ * sidebar doesn't list at all, the same filter the fetch above applies. One already there (another client's create event can arrive
73
+ * after, or twice, or alongside our own) is not added again - see `upsertFolder()`. */
74
+ const onFolderCreated = useCallback((folder) => setMailboxFolders((prev) => upsertFolder(prev, folder)), []);
75
+ /** A folder was renamed or changed type elsewhere. */
76
+ const onFolderChanged = useCallback((change) => setMailboxFolders((prev) => upsertFolder(prev, change)), []);
77
+ const onFolderDeleted = useCallback((folderUid) => setMailboxFolders((prev) => removeFolder(prev, folderUid)), []);
78
+ /** A mailbox's folders as the server just listed them (every count refresh does): folders the tree lacks - one created lazily on the
79
+ * first send, or by another client whose event never arrived - are added. */
80
+ const onFoldersListed = useCallback((listed) => setMailboxFolders((prev) => reconcileFolders(prev, listed)), []);
81
+ const askedFolderUidsRef = useRef(new Set());
82
+ const foldersRef = useRef(mailboxFolders);
83
+ foldersRef.current = mailboxFolders;
84
+ // New mail without a reload: push events for every folder, plus a safety-net poll. Held in state beside `mailboxFolders` rather than
85
+ // written into it, so a refreshed unread count never looks like a change of folders to the list on screen (which reloads, and forgets its
86
+ // selection, whenever the folders do). A pop-up (and, in the background, a desktop notification) for each new message the connection
87
+ // announces.
88
+ const notifications = useNewMailNotifications({ mailboxes, mailboxFolders, open });
89
+ const { live, folderCounts } = useMailLiveUpdates({
90
+ userUid: enabled ? userUid : undefined,
91
+ mailboxes,
92
+ mailboxFolders,
93
+ onFolderCreated,
94
+ onFolderChanged,
95
+ onFolderDeleted,
96
+ onFoldersListed,
97
+ onMessageCreated: notifications.announce,
98
+ onSendEvent: handleSendEvent,
99
+ });
100
+ const noteFolderUids = useCallback((folderUids) => {
101
+ const known = knownFolderUids(foldersRef.current);
102
+ const asked = askedFolderUidsRef.current;
103
+ let unknown = false;
104
+ for (const uid of folderUids) {
105
+ if (uid && !known.has(uid) && !asked.has(uid)) {
106
+ asked.add(uid);
107
+ unknown = true;
108
+ }
109
+ }
110
+ if (unknown) {
111
+ // The counts' read-back lists every mailbox's folders and files the new ones; a burst is one refresh.
112
+ folderCounts.refresh();
113
+ }
114
+ }, [folderCounts.refresh]);
115
+ // "Connection lost" for more than a few seconds is a subtle pop-up that goes when it is back.
116
+ usePushConnectionNotice(!!userUid && enabled);
117
+ // The Outbox indicator: a message being sent from this tab counts at once (before the server has heard of it), settled - or taken back - when
118
+ // the server answers (see `sendJob.ts`); once it accepted one, the folders are read again, since the Outbox may not have existed yet.
119
+ const latestRef = useRef({ mailboxFolders, folderCounts });
120
+ latestRef.current = { mailboxFolders, folderCounts };
121
+ useEffect(() => {
122
+ if (!userUid || !enabled) {
123
+ return;
124
+ }
125
+ return registerOutboxCounter((mailboxUid) => {
126
+ const { mailboxFolders: known, folderCounts: counts } = latestRef.current;
127
+ const outbox = known.find((entry) => entry.mailbox.uid === mailboxUid)?.folders.find((folder) => folder.type === "outbox");
128
+ return outbox ? counts.track(null, { folderUid: outbox.uid, flags: { read: true } }) : NO_TRACKER;
129
+ }, (mailboxUid) => {
130
+ // The server accepted a message: the Outbox (and, on an older server, Sent Items) may just have been made. The counts' read-back lists
131
+ // every mailbox's folders and files the new ones (`onFoldersListed`); this reads the one mailbox at once.
132
+ latestRef.current.folderCounts.refresh(0);
133
+ listFolders(mailboxUid).then(onFoldersListed, () => undefined);
134
+ });
135
+ }, [userUid, enabled, onFoldersListed]);
136
+ const outbox = useOutboxStatus(mailboxFolders, folderCounts.counts, live.tick, !!userUid && enabled);
137
+ return { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, notifications, outbox };
138
+ }
@@ -1,5 +1,8 @@
1
- import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
1
+ import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import { SendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
2
3
  import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
4
+ import { FolderCounts } from "./folderCounts.js";
5
+ import { FolderChange } from "./folderTree.js";
3
6
  /** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
4
7
  * that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
5
8
  export declare const LIVE_POLL_INTERVAL_MS = 45000;
@@ -16,9 +19,10 @@ export interface LiveUpdates {
16
19
  export declare const NO_LIVE_UPDATES: LiveUpdates;
17
20
  /**
18
21
  * The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
19
- * mailbox's Inbox - where mail lands - then each mailbox's other folders in the sidebar's own order, then the mailbox uids
20
- * themselves (which announce new folders). A folder is a channel of its own; subscribing to a mailbox does not deliver its
21
- * folders' events.
22
+ * mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
23
+ * they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
24
+ * just appeared is filed by its type like any other, so when the server's cap bites it is the last of them that go without live
25
+ * updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
22
26
  */
23
27
  export declare function pushChannelsFor(mailboxFolders: MailboxFolders[], mailboxes: Mailbox[]): string[];
24
28
  export interface UseMailLiveUpdatesOptions {
@@ -28,21 +32,37 @@ export interface UseMailLiveUpdatesOptions {
28
32
  mailboxFolders: MailboxFolders[];
29
33
  /** Called with a folder another client (or this one) created, so the sidebar shows it. Called only for one not already known. */
30
34
  onFolderCreated: (folder: Folder) => void;
35
+ /** Called when a folder's name or type was changed elsewhere (a `Folder` update event that says so - one that only carries counts is not this). */
36
+ onFolderChanged?: (change: FolderChange) => void;
37
+ /** Called with the uid of a folder that was deleted elsewhere. */
38
+ onFolderDeleted?: (folderUid: string) => void;
39
+ /** Called with a mailbox's folders as the server just listed them - on every refresh (a message event, the poll, a reconnect, the tab
40
+ * coming back, a send) - so a folder the tree does not know is found whichever way it appeared. */
41
+ onFoldersListed?: (folders: Folder[]) => void;
42
+ /** Called with each new message the push connection announces (a `create` event) - once per message, never for a list
43
+ * refetch, the initial load or a reconnect (nothing published while the socket was down is replayed). The caller decides
44
+ * whether it is worth telling the user about. */
45
+ onMessageCreated?: (message: Message) => void;
46
+ /** Called with the outcome of a message sent in the background (`send-succeeded`, `send-failed`, `send-retrying`). Every one also
47
+ * refreshes what is on screen (the Outbox and Sent Items lists and counts change with it). */
48
+ onSendEvent?: (event: SendEvent) => void;
31
49
  }
32
50
  export interface MailLiveUpdates {
33
51
  live: LiveUpdates;
34
- /** The latest unread count of each folder that has been refreshed since load, by folder uid - what the sidebar's badges show
35
- * instead of the count the folder list was loaded with. */
36
- unreadCounts: Record<string, number>;
52
+ /** The folder badges' numbers and how to change them - see `useFolderCounts()`. Kept in an overlay of their own, **not** in
53
+ * `mailboxFolders`, because the list effect reloads - and forgets the selection - whenever `mailboxFolders` changes. */
54
+ folderCounts: FolderCounts;
37
55
  }
38
56
  /**
39
57
  * Keeps Mail current without a page reload. One shared push connection per tab (`getPushClient()`) is subscribed to every
40
58
  * folder of every accessible mailbox; a message event for any of them, a reconnect, a poll of the safety net (every
41
59
  * `LIVE_POLL_INTERVAL_MS` while the tab is visible - and the only mechanism, silently, where the socket can't connect), the
42
60
  * tab coming back to the front or the browser coming back online, all end in the same debounced refresh: `live` is bumped
43
- * (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's unread count is re-read.
61
+ * (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's counts are re-read. A new message
62
+ * also bumps its folder's badge at once and is announced to `onMessageCreated`, and the server's `Folder` update events (the
63
+ * folder's real counts) are applied to the badges - see `useFolderCounts()`.
44
64
  *
45
65
  * Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
46
66
  * Renders nothing and does nothing where there is no window (server-side rendering).
47
67
  */
48
- export declare function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated }: UseMailLiveUpdatesOptions): MailLiveUpdates;
68
+ export declare function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onFoldersListed, onMessageCreated, onSendEvent, }: UseMailLiveUpdatesOptions): MailLiveUpdates;
@@ -3,9 +3,11 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { useEffect, useMemo, useRef, useState } from "react";
6
- import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
7
6
  import { getPushClient } from "@rapidmx/react-shared/mail/pushClient.js";
7
+ import { parseSendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
8
8
  import { SIGN_OUT_CHANNEL } from "../search/localIndexRpcClient.js";
9
+ import { useFolderCounts } from "./folderCounts.js";
10
+ import { sortFolders } from "./folderTree.js";
9
11
  /** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
10
12
  * that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
11
13
  export const LIVE_POLL_INTERVAL_MS = 45000;
@@ -18,19 +20,33 @@ const FOLDER_EVENT = /^Folder/;
18
20
  const MESSAGE_ACTIONS = new Set(["create", "update", "delete"]);
19
21
  /**
20
22
  * The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
21
- * mailbox's Inbox - where mail lands - then each mailbox's other folders in the sidebar's own order, then the mailbox uids
22
- * themselves (which announce new folders). A folder is a channel of its own; subscribing to a mailbox does not deliver its
23
- * folders' events.
23
+ * mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
24
+ * they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
25
+ * just appeared is filed by its type like any other, so when the server's cap bites it is the last of them that go without live
26
+ * updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
24
27
  */
25
28
  export function pushChannelsFor(mailboxFolders, mailboxes) {
26
29
  const inboxes = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "inbox"));
27
- const others = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type !== "inbox"));
28
- return [...new Set([...inboxes, ...others].map((folder) => folder.uid).concat(mailboxes.map((mailbox) => mailbox.uid)))];
30
+ const others = mailboxFolders.flatMap((entry) => sortFolders(entry.folders.filter((folder) => folder.type !== "inbox")));
31
+ return [...new Set([...inboxes.map((folder) => folder.uid), ...mailboxes.map((mailbox) => mailbox.uid), ...others.map((folder) => folder.uid)])];
29
32
  }
30
33
  function isFolder(value) {
31
34
  const folder = value;
32
35
  return !!folder && typeof folder.uid === "string" && typeof folder.mailboxUid === "string" && typeof folder.type === "string";
33
36
  }
37
+ /** A `Folder` update event that says the folder's name or type changed (and names the folder): the counts-only ones the server publishes after every message change do not. */
38
+ function isFolderChange(value) {
39
+ const change = value;
40
+ return !!change && typeof change.uid === "string" && (typeof change.name === "string" || typeof change.type === "string");
41
+ }
42
+ function isMessage(value) {
43
+ const message = value;
44
+ return (!!message &&
45
+ typeof message.uid === "string" &&
46
+ typeof message.folderUid === "string" &&
47
+ typeof message.flags === "object" &&
48
+ message.flags !== null);
49
+ }
34
50
  /** The folder a message event is about: the message's own `folderUid`, else the channel it arrived on (when the server said). */
35
51
  function folderOfMessageEvent(event) {
36
52
  const data = event.data;
@@ -44,17 +60,20 @@ function folderOfMessageEvent(event) {
44
60
  * folder of every accessible mailbox; a message event for any of them, a reconnect, a poll of the safety net (every
45
61
  * `LIVE_POLL_INTERVAL_MS` while the tab is visible - and the only mechanism, silently, where the socket can't connect), the
46
62
  * tab coming back to the front or the browser coming back online, all end in the same debounced refresh: `live` is bumped
47
- * (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's unread count is re-read.
63
+ * (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's counts are re-read. A new message
64
+ * also bumps its folder's badge at once and is announced to `onMessageCreated`, and the server's `Folder` update events (the
65
+ * folder's real counts) are applied to the badges - see `useFolderCounts()`.
48
66
  *
49
67
  * Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
50
68
  * Renders nothing and does nothing where there is no window (server-side rendering).
51
69
  */
52
- export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated }) {
70
+ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onFoldersListed, onMessageCreated, onSendEvent, }) {
53
71
  const [live, setLive] = useState(NO_LIVE_UPDATES);
54
- const [unreadCounts, setUnreadCounts] = useState({});
72
+ const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
73
+ const folderCounts = useFolderCounts(mailboxes, folders, onFoldersListed);
55
74
  // The latest inputs, for the long-lived listeners below.
56
- const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated });
57
- latestRef.current = { mailboxes, mailboxFolders, onFolderCreated };
75
+ const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts });
76
+ latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts };
58
77
  const channels = useMemo(() => pushChannelsFor(mailboxFolders, mailboxes), [mailboxFolders, mailboxes]);
59
78
  const channelKey = channels.join("|");
60
79
  const channelsRef = useRef(channels);
@@ -68,25 +87,7 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
68
87
  let timer;
69
88
  let pendingFolders = new Set();
70
89
  let pendingUnknown = false;
71
- let countsRun = 0;
72
90
  let everOpen = false;
73
- async function refreshCounts() {
74
- const run = ++countsRun;
75
- const results = await Promise.all(latestRef.current.mailboxes.map((mailbox) => listFolders(mailbox.uid).catch(() => undefined)));
76
- if (stopped || run !== countsRun) {
77
- return;
78
- }
79
- const fresh = {};
80
- for (const folders of results) {
81
- for (const folder of folders ?? []) {
82
- fresh[folder.uid] = folder.unreadCount;
83
- }
84
- }
85
- setUnreadCounts((previous) => {
86
- const changed = Object.entries(fresh).some(([uid, count]) => previous[uid] !== count);
87
- return changed ? { ...previous, ...fresh } : previous;
88
- });
89
- }
90
91
  // Never runs once `stopped`: whatever sets it clears the timer first.
91
92
  function flush() {
92
93
  timer = undefined;
@@ -94,7 +95,7 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
94
95
  pendingFolders = new Set();
95
96
  pendingUnknown = false;
96
97
  setLive((previous) => ({ tick: previous.tick + 1, folderUids }));
97
- void refreshCounts();
98
+ latestRef.current.folderCounts.refresh(0);
98
99
  }
99
100
  /** Asks for a refresh: of the given folders, or (no argument) of whatever may have changed. */
100
101
  function schedule(folderUid) {
@@ -110,14 +111,41 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
110
111
  timer ?? (timer = setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS));
111
112
  }
112
113
  const offEvent = client.onEvent((event) => {
113
- if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
114
+ const sendEvent = parseSendEvent(event);
115
+ if (sendEvent) {
116
+ // A queued message was relayed (or failed to be): it left Outbox for Sent Items, or stayed there failed - every list and count may have changed.
117
+ latestRef.current.onSendEvent?.(sendEvent);
118
+ schedule();
119
+ }
120
+ else if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
121
+ if (event.action === "create" && isMessage(event.data)) {
122
+ // Once per message, whatever else the event triggers: the badge first, then whoever announces it.
123
+ if (latestRef.current.folderCounts.noteCreated(event.data)) {
124
+ latestRef.current.onMessageCreated?.(event.data);
125
+ }
126
+ }
114
127
  schedule(folderOfMessageEvent(event));
115
128
  }
116
129
  else if (FOLDER_EVENT.test(event.type) && event.action === "create" && isFolder(event.data)) {
130
+ // A folder made by the server (on first use, or by another client) or by this one: filed in the sidebar and subscribed to at once (the
131
+ // channel list follows the tree), and its real counts read - all without a reload.
117
132
  const folder = event.data;
118
133
  const known = latestRef.current.mailboxFolders.some((entry) => entry.folders.some((f) => f.uid === folder.uid));
119
134
  if (!known) {
120
135
  latestRef.current.onFolderCreated(folder);
136
+ latestRef.current.folderCounts.refresh(0);
137
+ }
138
+ }
139
+ else if (FOLDER_EVENT.test(event.type) && event.action === "update") {
140
+ if (isFolderChange(event.data)) {
141
+ latestRef.current.onFolderChanged?.(event.data);
142
+ }
143
+ latestRef.current.folderCounts.applyFolderEvent(event.data);
144
+ }
145
+ else if (FOLDER_EVENT.test(event.type) && event.action === "delete") {
146
+ const uid = event.data?.uid;
147
+ if (typeof uid === "string") {
148
+ latestRef.current.onFolderDeleted?.(uid);
121
149
  }
122
150
  }
123
151
  });
@@ -176,5 +204,5 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
176
204
  getPushClient().setChannels(channels);
177
205
  }
178
206
  }, [userUid, channelKey]);
179
- return { live, unreadCounts };
207
+ return { live, folderCounts };
180
208
  }
@@ -0,0 +1,12 @@
1
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /**
3
+ * Marks a message read when it is opened in a reading pane, through `setReadState()`: the row and the folder badge change at
4
+ * once. `onPatched` is handed each copy of the message as it changes (the optimistic one, then the server's, or the original
5
+ * again if the server refused).
6
+ *
7
+ * Replaces `@rapidmx/react-shared`'s `useMarkMessageRead`, which waits for the server before telling anyone and drops that
8
+ * answer if the message changes meanwhile - which the optimistic copy itself does. A message is asked for once each time it is
9
+ * opened: one the reader then marks unread stays unread while it is open, and one whose request failed is not retried in a loop
10
+ * (its own revert changes it again), only when it is opened again.
11
+ */
12
+ export declare function useMarkMessageRead(message: Message | null, onPatched: (updated: Message, previous?: Message) => void): void;
@@ -0,0 +1,44 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useRef } from "react";
6
+ import { useMailShell } from "../components/mail/layout/MailShell.js";
7
+ import { setReadState } from "./messageReadState.js";
8
+ /**
9
+ * Marks a message read when it is opened in a reading pane, through `setReadState()`: the row and the folder badge change at
10
+ * once. `onPatched` is handed each copy of the message as it changes (the optimistic one, then the server's, or the original
11
+ * again if the server refused).
12
+ *
13
+ * Replaces `@rapidmx/react-shared`'s `useMarkMessageRead`, which waits for the server before telling anyone and drops that
14
+ * answer if the message changes meanwhile - which the optimistic copy itself does. A message is asked for once each time it is
15
+ * opened: one the reader then marks unread stays unread while it is open, and one whose request failed is not retried in a loop
16
+ * (its own revert changes it again), only when it is opened again.
17
+ */
18
+ export function useMarkMessageRead(message, onPatched) {
19
+ const { trackMessageChange } = useMailShell();
20
+ const openUidRef = useRef(undefined);
21
+ const requestedRef = useRef(new Set());
22
+ const latestRef = useRef({ onPatched, trackMessageChange });
23
+ latestRef.current = { onPatched, trackMessageChange };
24
+ useEffect(() => {
25
+ if (message?.uid !== openUidRef.current) {
26
+ // Another message (or none) is open now: whatever was asked about the last one is over.
27
+ openUidRef.current = message?.uid;
28
+ requestedRef.current.clear();
29
+ }
30
+ if (!message || requestedRef.current.has(message.uid)) {
31
+ return;
32
+ }
33
+ // Every opening is asked about once - a message that is already read included, so that marking it unread while it is open (the
34
+ // keyboard's Ctrl+U) doesn't make this run again and read it straight back.
35
+ requestedRef.current.add(message.uid);
36
+ if (message.flags.read === true) {
37
+ return;
38
+ }
39
+ void setReadState(message, true, {
40
+ patch: (updated, previous) => latestRef.current.onPatched(updated, previous),
41
+ track: (previous, next) => latestRef.current.trackMessageChange(previous, next),
42
+ });
43
+ }, [message]);
44
+ }
@@ -0,0 +1,35 @@
1
+ import type { Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
3
+ /** How long a new-mail pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
4
+ export declare const TOAST_DURATION_MS = 8000;
5
+ /** At most this many desktop notifications in `DESKTOP_BURST_WINDOW_MS`: a flood of mail is a few notifications, not a hundred. */
6
+ export declare const DESKTOP_BURST_LIMIT = 5;
7
+ export declare const DESKTOP_BURST_WINDOW_MS = 30000;
8
+ export interface NewMailNotifications {
9
+ /** Whether the next pop-up should also offer to turn on desktop notifications: the browser can, has not been asked,
10
+ * and the user has not said "Not now". */
11
+ offerDesktop: boolean;
12
+ /** The offer's "Turn on desktop notifications": asks the browser. Must be called from a click. */
13
+ enableDesktop(): Promise<void>;
14
+ /** The offer's "Not now": remembered, so it is not made again. */
15
+ declineDesktop(): void;
16
+ /** Announces a message that just arrived - if it is worth it (see `shouldAnnounce()`) and pop-ups are on. Stable. */
17
+ announce(message: Message): void;
18
+ }
19
+ export interface UseNewMailNotificationsOptions {
20
+ mailboxes: Mailbox[];
21
+ mailboxFolders: MailboxFolders[];
22
+ /** Opens a message from a desktop notification's click. Defaults to a plain navigation. */
23
+ open?: (href: string) => void;
24
+ }
25
+ /**
26
+ * New-mail announcements: an in-app pop-up for each message that arrives (a `mail` notification - see `notifications/store.ts`, which owns the
27
+ * stack, its limit of three and the clock), and - while the tab
28
+ * is in the background and the user has allowed it - a desktop notification with the same content, one per message however
29
+ * many events name it. `announce()` is what `useMailLiveUpdates()`'s `onMessageCreated` calls; nothing else feeds it, so a
30
+ * page load, a list refetch and a reconnect never announce anything.
31
+ *
32
+ * Nothing is ever asked of the browser on load: permission is requested only by `enableDesktop()`, from the offer in the
33
+ * first pop-up, or from Settings. What the user answered is remembered in `localStorage` (see `newMailNotifications.ts`).
34
+ */
35
+ export declare function useNewMailNotifications({ mailboxes, mailboxFolders, open }: UseNewMailNotificationsOptions): NewMailNotifications;
@@ -0,0 +1,130 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { desktopPermission, getDesktopOfferDismissed, getNewMailPopupsEnabled, noticeFor, noticeSender, ownAddressesOf, requestDesktopPermission, setDesktopOfferDismissed, shouldAnnounce, } from "./newMailNotifications.js";
7
+ import { getNotificationsSnapshot, notify } from "../notifications/store.js";
8
+ /** How long a new-mail pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
9
+ export const TOAST_DURATION_MS = 8000;
10
+ /** At most this many desktop notifications in `DESKTOP_BURST_WINDOW_MS`: a flood of mail is a few notifications, not a hundred. */
11
+ export const DESKTOP_BURST_LIMIT = 5;
12
+ export const DESKTOP_BURST_WINDOW_MS = 30000;
13
+ /** How many announced message uids are remembered, so an event delivered twice is announced once. */
14
+ const ANNOUNCED_LIMIT = 500;
15
+ /** Whether the user is not looking at this tab: it is hidden, or another window has the focus. */
16
+ function tabIsInBackground() {
17
+ return document.visibilityState === "hidden" || !document.hasFocus();
18
+ }
19
+ function defaultOpen(href) {
20
+ window.location.href = href;
21
+ }
22
+ /**
23
+ * New-mail announcements: an in-app pop-up for each message that arrives (a `mail` notification - see `notifications/store.ts`, which owns the
24
+ * stack, its limit of three and the clock), and - while the tab
25
+ * is in the background and the user has allowed it - a desktop notification with the same content, one per message however
26
+ * many events name it. `announce()` is what `useMailLiveUpdates()`'s `onMessageCreated` calls; nothing else feeds it, so a
27
+ * page load, a list refetch and a reconnect never announce anything.
28
+ *
29
+ * Nothing is ever asked of the browser on load: permission is requested only by `enableDesktop()`, from the offer in the
30
+ * first pop-up, or from Settings. What the user answered is remembered in `localStorage` (see `newMailNotifications.ts`).
31
+ */
32
+ export function useNewMailNotifications({ mailboxes, mailboxFolders, open = defaultOpen }) {
33
+ const [permission, setPermission] = useState("unsupported");
34
+ const [offerDismissed, setOfferDismissed] = useState(true);
35
+ const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
36
+ const ownAddresses = useMemo(() => ownAddressesOf(mailboxes), [mailboxes]);
37
+ const latestRef = useRef({ folders, ownAddresses, open });
38
+ latestRef.current = { folders, ownAddresses, open };
39
+ const announcedRef = useRef(new Set());
40
+ const desktopShownRef = useRef([]);
41
+ // The browser's own state is only read on the client, after hydration, so the server and first client render agree.
42
+ useEffect(() => {
43
+ setPermission(desktopPermission());
44
+ setOfferDismissed(getDesktopOfferDismissed());
45
+ }, []);
46
+ const showDesktop = useCallback((notice) => {
47
+ const now = Date.now();
48
+ desktopShownRef.current = desktopShownRef.current.filter((at) => now - at < DESKTOP_BURST_WINDOW_MS);
49
+ if (desktopShownRef.current.length >= DESKTOP_BURST_LIMIT) {
50
+ return;
51
+ }
52
+ try {
53
+ const notification = new Notification(noticeSender(notice), {
54
+ body: [notice.subject, notice.preview].filter(Boolean).join("\n"),
55
+ // One per message: a duplicate (another tab of this app, a re-delivered event) replaces instead of stacking.
56
+ tag: notice.uid,
57
+ });
58
+ desktopShownRef.current.push(now);
59
+ notification.onclick = () => {
60
+ window.focus();
61
+ notification.close();
62
+ latestRef.current.open(notice.href);
63
+ };
64
+ }
65
+ catch {
66
+ // Some browsers refuse `new Notification()` outright (a mobile one wanting a service worker): the pop-up is enough.
67
+ }
68
+ }, []);
69
+ const enableDesktop = useCallback(async () => {
70
+ const answer = await requestDesktopPermission();
71
+ setPermission(answer);
72
+ // Answered either way, so the offer goes: on granted it has done its job, on denied it can't be made again.
73
+ setDesktopOfferDismissed(true);
74
+ setOfferDismissed(true);
75
+ }, []);
76
+ const declineDesktop = useCallback(() => {
77
+ setDesktopOfferDismissed(true);
78
+ setOfferDismissed(true);
79
+ }, []);
80
+ // The offer's current state and answers, for the stable `announce` below.
81
+ const offerDesktop = permission === "default" && !offerDismissed;
82
+ const offerRef = useRef({ offered: offerDesktop, enable: enableDesktop, decline: declineDesktop });
83
+ offerRef.current = { offered: offerDesktop, enable: enableDesktop, decline: declineDesktop };
84
+ const announce = useCallback((message) => {
85
+ if (!getNewMailPopupsEnabled() || announcedRef.current.has(message.uid)) {
86
+ return;
87
+ }
88
+ const { folders: knownFolders, ownAddresses: own } = latestRef.current;
89
+ if (!shouldAnnounce(message, { folders: knownFolders, ownAddresses: own })) {
90
+ return;
91
+ }
92
+ announcedRef.current.add(message.uid);
93
+ if (announcedRef.current.size > ANNOUNCED_LIMIT) {
94
+ announcedRef.current.delete(announcedRef.current.values().next().value);
95
+ }
96
+ const notice = noticeFor(message);
97
+ const offer = offerRef.current;
98
+ notify({
99
+ id: `mail:${notice.uid}`,
100
+ kind: "mail",
101
+ title: notice.senderName || notice.senderAddress,
102
+ subtitle: notice.senderName ? `<${notice.senderAddress}>` : undefined,
103
+ message: notice.subject,
104
+ preview: notice.preview || undefined,
105
+ href: notice.href,
106
+ timeoutMs: TOAST_DURATION_MS,
107
+ // Actions make a notification sticky; a new-mail pop-up with the offer still goes by itself.
108
+ sticky: false,
109
+ // The offer is made once at a time: on a pop-up only while none of the pop-ups still showing carries it.
110
+ ...(offer.offered && !getNotificationsSnapshot().visible.some((item) => item.kind === "mail" && item.actions.length > 0)
111
+ ? {
112
+ hint: "Get a desktop notification when mail arrives while this tab is in the background.",
113
+ actions: [
114
+ { label: "Turn on desktop notifications", onClick: () => void offer.enable() },
115
+ { label: "Not now", onClick: offer.decline },
116
+ ],
117
+ }
118
+ : {}),
119
+ });
120
+ if (desktopPermission() === "granted" && tabIsInBackground()) {
121
+ showDesktop(notice);
122
+ }
123
+ }, [showDesktop]);
124
+ return {
125
+ offerDesktop,
126
+ enableDesktop,
127
+ declineDesktop,
128
+ announce,
129
+ };
130
+ }
@@ -0,0 +1,18 @@
1
+ /** `title` with `count` unread in front - `(3) Acme: Mail` - or as it is when there are none. */
2
+ export declare function titleWithUnread(title: string, count: number): string;
3
+ export interface UseUnreadTitleOptions {
4
+ /** Does nothing while false - for the one of two owners that isn't. Default true. */
5
+ enabled?: boolean;
6
+ /** Changes whenever something else has rewritten the title (the app frame sets it on every page change), so the count is put in front of
7
+ * the new one. */
8
+ resetKey?: unknown;
9
+ }
10
+ /**
11
+ * Keeps the browser tab's title in step with the unread count, as Outlook on the web does - `(3) Acme: Mail` - so mail that
12
+ * arrives while the tab is in the background can be seen from the tab strip. The count is put in front of whatever the title is
13
+ * (the page sets its own, from the branding) and taken off again when it drops to zero or the component goes away.
14
+ *
15
+ * Order matters when the same component also sets the title: declare this hook after the effect that does, so that on a change of `resetKey`
16
+ * the count is taken off the old title, the new title is written, and the count goes back in front of it.
17
+ */
18
+ export declare function useUnreadTitle(count: number, { enabled, resetKey }?: UseUnreadTitleOptions): void;