@rapidmx/web-client 0.15.1 → 0.17.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 (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -1,277 +1,277 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import { useEffect, useMemo, useRef, useState } from "react";
6
- import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
7
- import { getPushClient, PushEvent } from "@rapidmx/react-shared/mail/pushClient.js";
8
- import { SendEvent, parseSendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
9
- import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
10
- import { SIGN_OUT_CHANNEL } from "../search/localIndexRpcClient.js";
11
- import { FolderCounts, useFolderCounts } from "./folderCounts.js";
12
- import { FolderChange, sortFolders } from "./folderTree.js";
13
-
14
- /** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
15
- * that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
16
- export const LIVE_POLL_INTERVAL_MS = 45_000;
17
-
18
- /** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
19
- export const LIVE_EVENT_DEBOUNCE_MS = 400;
20
-
21
- /** What changed, as far as a list on screen is concerned. */
22
- export interface LiveUpdates {
23
- /** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
24
- tick: number;
25
- /** The folders the events behind the latest bump touched - `null` when that isn't known (a poll, a reconnect, the tab
26
- * coming back to the front), meaning any folder may have changed. */
27
- folderUids: ReadonlySet<string> | null;
28
- }
29
-
30
- export const NO_LIVE_UPDATES: LiveUpdates = { tick: 0, folderUids: null };
31
-
32
- /** The class names `NotificationUtils.sendMessage()` publishes are the model's (`MessageMongo`, `MessageSQL`, `FolderMongo`). */
33
- const MESSAGE_EVENT = /^Message/;
34
- const FOLDER_EVENT = /^Folder/;
35
- const MESSAGE_ACTIONS = new Set(["create", "update", "delete"]);
36
-
37
- /**
38
- * The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
39
- * mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
40
- * they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
41
- * 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
42
- * updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
43
- */
44
- export function pushChannelsFor(mailboxFolders: MailboxFolders[], mailboxes: Mailbox[]): string[] {
45
- const inboxes = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "inbox"));
46
- const others = mailboxFolders.flatMap((entry) => sortFolders(entry.folders.filter((folder) => folder.type !== "inbox")));
47
- return [...new Set([...inboxes.map((folder) => folder.uid), ...mailboxes.map((mailbox) => mailbox.uid), ...others.map((folder) => folder.uid)])];
48
- }
49
-
50
- function isFolder(value: unknown): value is Folder {
51
- const folder = value as Partial<Folder> | null;
52
- return !!folder && typeof folder.uid === "string" && typeof folder.mailboxUid === "string" && typeof folder.type === "string";
53
- }
54
-
55
- /** 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. */
56
- function isFolderChange(value: unknown): value is FolderChange {
57
- const change = value as Partial<FolderChange> | null;
58
- return !!change && typeof change.uid === "string" && (typeof change.name === "string" || typeof change.type === "string");
59
- }
60
-
61
- function isMessage(value: unknown): value is Message {
62
- const message = value as Partial<Message> | null;
63
- return (
64
- !!message &&
65
- typeof message.uid === "string" &&
66
- typeof message.folderUid === "string" &&
67
- typeof message.flags === "object" &&
68
- message.flags !== null
69
- );
70
- }
71
-
72
- /** The folder a message event is about: the message's own `folderUid`, else the channel it arrived on (when the server said). */
73
- function folderOfMessageEvent(event: PushEvent): string | undefined {
74
- const data = event.data as { folderUid?: unknown } | null | undefined;
75
- if (typeof data?.folderUid === "string") {
76
- return data.folderUid;
77
- }
78
- return event.channel;
79
- }
80
-
81
- export interface UseMailLiveUpdatesOptions {
82
- /** Nothing runs without a signed-in user. */
83
- userUid?: string;
84
- mailboxes: Mailbox[];
85
- mailboxFolders: MailboxFolders[];
86
- /** Called with a folder another client (or this one) created, so the sidebar shows it. Called only for one not already known. */
87
- onFolderCreated: (folder: Folder) => void;
88
- /** 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). */
89
- onFolderChanged?: (change: FolderChange) => void;
90
- /** Called with the uid of a folder that was deleted elsewhere. */
91
- onFolderDeleted?: (folderUid: string) => void;
92
- /** Called with a mailbox's folders as the server just listed them - on every refresh (a message event, the poll, a reconnect, the tab
93
- * coming back, a send) - so a folder the tree does not know is found whichever way it appeared. */
94
- onFoldersListed?: (folders: Folder[]) => void;
95
- /** Called with each new message the push connection announces (a `create` event) - once per message, never for a list
96
- * refetch, the initial load or a reconnect (nothing published while the socket was down is replayed). The caller decides
97
- * whether it is worth telling the user about. */
98
- onMessageCreated?: (message: Message) => void;
99
- /** Called with the outcome of a message sent in the background (`send-succeeded`, `send-failed`, `send-retrying`). Every one also
100
- * refreshes what is on screen (the Outbox and Sent Items lists and counts change with it). */
101
- onSendEvent?: (event: SendEvent) => void;
102
- }
103
-
104
- export interface MailLiveUpdates {
105
- live: LiveUpdates;
106
- /** The folder badges' numbers and how to change them - see `useFolderCounts()`. Kept in an overlay of their own, **not** in
107
- * `mailboxFolders`, because the list effect reloads - and forgets the selection - whenever `mailboxFolders` changes. */
108
- folderCounts: FolderCounts;
109
- }
110
-
111
- /**
112
- * Keeps Mail current without a page reload. One shared push connection per tab (`getPushClient()`) is subscribed to every
113
- * folder of every accessible mailbox; a message event for any of them, a reconnect, a poll of the safety net (every
114
- * `LIVE_POLL_INTERVAL_MS` while the tab is visible - and the only mechanism, silently, where the socket can't connect), the
115
- * tab coming back to the front or the browser coming back online, all end in the same debounced refresh: `live` is bumped
116
- * (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's counts are re-read. A new message
117
- * also bumps its folder's badge at once and is announced to `onMessageCreated`, and the server's `Folder` update events (the
118
- * folder's real counts) are applied to the badges - see `useFolderCounts()`.
119
- *
120
- * Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
121
- * Renders nothing and does nothing where there is no window (server-side rendering).
122
- */
123
- export function useMailLiveUpdates({
124
- userUid,
125
- mailboxes,
126
- mailboxFolders,
127
- onFolderCreated,
128
- onFolderChanged,
129
- onFolderDeleted,
130
- onFoldersListed,
131
- onMessageCreated,
132
- onSendEvent,
133
- }: UseMailLiveUpdatesOptions): MailLiveUpdates {
134
- const [live, setLive] = useState<LiveUpdates>(NO_LIVE_UPDATES);
135
- const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
136
- const folderCounts = useFolderCounts(mailboxes, folders, onFoldersListed);
137
- // The latest inputs, for the long-lived listeners below.
138
- const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts });
139
- latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts };
140
-
141
- const channels = useMemo(() => pushChannelsFor(mailboxFolders, mailboxes), [mailboxFolders, mailboxes]);
142
- const channelKey = channels.join("|");
143
- const channelsRef = useRef(channels);
144
- channelsRef.current = channels;
145
-
146
- useEffect(() => {
147
- if (!userUid) {
148
- return;
149
- }
150
- const client = getPushClient();
151
- let stopped = false;
152
- let timer: ReturnType<typeof setTimeout> | undefined;
153
- let pendingFolders = new Set<string>();
154
- let pendingUnknown = false;
155
- let everOpen = false;
156
-
157
- // Never runs once `stopped`: whatever sets it clears the timer first.
158
- function flush() {
159
- timer = undefined;
160
- const folderUids = pendingUnknown ? null : new Set(pendingFolders);
161
- pendingFolders = new Set();
162
- pendingUnknown = false;
163
- setLive((previous) => ({ tick: previous.tick + 1, folderUids }));
164
- latestRef.current.folderCounts.refresh(0);
165
- }
166
-
167
- /** Asks for a refresh: of the given folders, or (no argument) of whatever may have changed. */
168
- function schedule(folderUid?: string) {
169
- if (stopped) {
170
- return;
171
- }
172
- if (folderUid) {
173
- pendingFolders.add(folderUid);
174
- } else {
175
- pendingUnknown = true;
176
- }
177
- timer ??= setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS);
178
- }
179
-
180
- const offEvent = client.onEvent((event) => {
181
- const sendEvent = parseSendEvent(event);
182
- if (sendEvent) {
183
- // 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.
184
- latestRef.current.onSendEvent?.(sendEvent);
185
- schedule();
186
- } else if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
187
- if (event.action === "create" && isMessage(event.data)) {
188
- // Once per message, whatever else the event triggers: the badge first, then whoever announces it.
189
- if (latestRef.current.folderCounts.noteCreated(event.data)) {
190
- latestRef.current.onMessageCreated?.(event.data);
191
- }
192
- }
193
- schedule(folderOfMessageEvent(event));
194
- } else if (FOLDER_EVENT.test(event.type) && event.action === "create" && isFolder(event.data)) {
195
- // 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
196
- // channel list follows the tree), and its real counts read - all without a reload.
197
- const folder = event.data;
198
- const known = latestRef.current.mailboxFolders.some((entry) => entry.folders.some((f) => f.uid === folder.uid));
199
- if (!known) {
200
- latestRef.current.onFolderCreated(folder);
201
- latestRef.current.folderCounts.refresh(0);
202
- }
203
- } else if (FOLDER_EVENT.test(event.type) && event.action === "update") {
204
- if (isFolderChange(event.data)) {
205
- latestRef.current.onFolderChanged?.(event.data);
206
- }
207
- latestRef.current.folderCounts.applyFolderEvent(event.data);
208
- } else if (FOLDER_EVENT.test(event.type) && event.action === "delete") {
209
- const uid = (event.data as { uid?: unknown } | null | undefined)?.uid;
210
- if (typeof uid === "string") {
211
- latestRef.current.onFolderDeleted?.(uid);
212
- }
213
- }
214
- });
215
- // Anything published while the socket was down is gone for good, so every reconnect is followed by a refresh.
216
- const offStatus = client.onStatus((status) => {
217
- if (status === "open") {
218
- if (everOpen) {
219
- schedule();
220
- }
221
- everOpen = true;
222
- }
223
- });
224
-
225
- const poll = setInterval(() => {
226
- if (document.visibilityState === "visible") {
227
- schedule();
228
- }
229
- }, LIVE_POLL_INTERVAL_MS);
230
- function refreshNow() {
231
- if (document.visibilityState === "visible") {
232
- client.reconnectNow();
233
- schedule();
234
- }
235
- }
236
- function handleOnline() {
237
- client.reconnectNow();
238
- schedule();
239
- }
240
- document.addEventListener("visibilitychange", refreshNow);
241
- window.addEventListener("focus", refreshNow);
242
- window.addEventListener("online", handleOnline);
243
-
244
- // A sign-out, in this tab or another, ended the session behind the socket: close it for good and stop refreshing.
245
- const signOut = typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel(SIGN_OUT_CHANNEL);
246
- signOut?.addEventListener("message", (event: MessageEvent<{ type?: string }>) => {
247
- if (event.data?.type === "sign-out") {
248
- stopped = true;
249
- clearTimeout(timer);
250
- client.close();
251
- }
252
- });
253
-
254
- client.setChannels(channelsRef.current);
255
- client.start();
256
-
257
- return () => {
258
- stopped = true;
259
- clearTimeout(timer);
260
- clearInterval(poll);
261
- offEvent();
262
- offStatus();
263
- document.removeEventListener("visibilitychange", refreshNow);
264
- window.removeEventListener("focus", refreshNow);
265
- window.removeEventListener("online", handleOnline);
266
- signOut?.close();
267
- };
268
- }, [userUid]);
269
-
270
- useEffect(() => {
271
- if (userUid) {
272
- getPushClient().setChannels(channels);
273
- }
274
- }, [userUid, channelKey]);
275
-
276
- return { live, folderCounts };
277
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
+ import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { getPushClient, PushEvent } from "@rapidmx/react-shared/mail/pushClient.js";
8
+ import { SendEvent, parseSendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
9
+ import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
10
+ import { SIGN_OUT_CHANNEL } from "../search/localIndexRpcClient.js";
11
+ import { FolderCounts, useFolderCounts } from "./folderCounts.js";
12
+ import { FolderChange, sortFolders } from "./folderTree.js";
13
+
14
+ /** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
15
+ * that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
16
+ export const LIVE_POLL_INTERVAL_MS = 45_000;
17
+
18
+ /** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
19
+ export const LIVE_EVENT_DEBOUNCE_MS = 400;
20
+
21
+ /** What changed, as far as a list on screen is concerned. */
22
+ export interface LiveUpdates {
23
+ /** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
24
+ tick: number;
25
+ /** The folders the events behind the latest bump touched - `null` when that isn't known (a poll, a reconnect, the tab
26
+ * coming back to the front), meaning any folder may have changed. */
27
+ folderUids: ReadonlySet<string> | null;
28
+ }
29
+
30
+ export const NO_LIVE_UPDATES: LiveUpdates = { tick: 0, folderUids: null };
31
+
32
+ /** The class names `NotificationUtils.sendMessage()` publishes are the model's (`MessageMongo`, `MessageSQL`, `FolderMongo`). */
33
+ const MESSAGE_EVENT = /^Message/;
34
+ const FOLDER_EVENT = /^Folder/;
35
+ const MESSAGE_ACTIONS = new Set(["create", "update", "delete"]);
36
+
37
+ /**
38
+ * The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
39
+ * mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
40
+ * they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
41
+ * 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
42
+ * updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
43
+ */
44
+ export function pushChannelsFor(mailboxFolders: MailboxFolders[], mailboxes: Mailbox[]): string[] {
45
+ const inboxes = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "inbox"));
46
+ const others = mailboxFolders.flatMap((entry) => sortFolders(entry.folders.filter((folder) => folder.type !== "inbox")));
47
+ return [...new Set([...inboxes.map((folder) => folder.uid), ...mailboxes.map((mailbox) => mailbox.uid), ...others.map((folder) => folder.uid)])];
48
+ }
49
+
50
+ function isFolder(value: unknown): value is Folder {
51
+ const folder = value as Partial<Folder> | null;
52
+ return !!folder && typeof folder.uid === "string" && typeof folder.mailboxUid === "string" && typeof folder.type === "string";
53
+ }
54
+
55
+ /** 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. */
56
+ function isFolderChange(value: unknown): value is FolderChange {
57
+ const change = value as Partial<FolderChange> | null;
58
+ return !!change && typeof change.uid === "string" && (typeof change.name === "string" || typeof change.type === "string");
59
+ }
60
+
61
+ function isMessage(value: unknown): value is Message {
62
+ const message = value as Partial<Message> | null;
63
+ return (
64
+ !!message &&
65
+ typeof message.uid === "string" &&
66
+ typeof message.folderUid === "string" &&
67
+ typeof message.flags === "object" &&
68
+ message.flags !== null
69
+ );
70
+ }
71
+
72
+ /** The folder a message event is about: the message's own `folderUid`, else the channel it arrived on (when the server said). */
73
+ function folderOfMessageEvent(event: PushEvent): string | undefined {
74
+ const data = event.data as { folderUid?: unknown } | null | undefined;
75
+ if (typeof data?.folderUid === "string") {
76
+ return data.folderUid;
77
+ }
78
+ return event.channel;
79
+ }
80
+
81
+ export interface UseMailLiveUpdatesOptions {
82
+ /** Nothing runs without a signed-in user. */
83
+ userUid?: string;
84
+ mailboxes: Mailbox[];
85
+ mailboxFolders: MailboxFolders[];
86
+ /** Called with a folder another client (or this one) created, so the sidebar shows it. Called only for one not already known. */
87
+ onFolderCreated: (folder: Folder) => void;
88
+ /** 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). */
89
+ onFolderChanged?: (change: FolderChange) => void;
90
+ /** Called with the uid of a folder that was deleted elsewhere. */
91
+ onFolderDeleted?: (folderUid: string) => void;
92
+ /** Called with a mailbox's folders as the server just listed them - on every refresh (a message event, the poll, a reconnect, the tab
93
+ * coming back, a send) - so a folder the tree does not know is found whichever way it appeared. */
94
+ onFoldersListed?: (folders: Folder[]) => void;
95
+ /** Called with each new message the push connection announces (a `create` event) - once per message, never for a list
96
+ * refetch, the initial load or a reconnect (nothing published while the socket was down is replayed). The caller decides
97
+ * whether it is worth telling the user about. */
98
+ onMessageCreated?: (message: Message) => void;
99
+ /** Called with the outcome of a message sent in the background (`send-succeeded`, `send-failed`, `send-retrying`). Every one also
100
+ * refreshes what is on screen (the Outbox and Sent Items lists and counts change with it). */
101
+ onSendEvent?: (event: SendEvent) => void;
102
+ }
103
+
104
+ export interface MailLiveUpdates {
105
+ live: LiveUpdates;
106
+ /** The folder badges' numbers and how to change them - see `useFolderCounts()`. Kept in an overlay of their own, **not** in
107
+ * `mailboxFolders`, because the list effect reloads - and forgets the selection - whenever `mailboxFolders` changes. */
108
+ folderCounts: FolderCounts;
109
+ }
110
+
111
+ /**
112
+ * Keeps Mail current without a page reload. One shared push connection per tab (`getPushClient()`) is subscribed to every
113
+ * folder of every accessible mailbox; a message event for any of them, a reconnect, a poll of the safety net (every
114
+ * `LIVE_POLL_INTERVAL_MS` while the tab is visible - and the only mechanism, silently, where the socket can't connect), the
115
+ * tab coming back to the front or the browser coming back online, all end in the same debounced refresh: `live` is bumped
116
+ * (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's counts are re-read. A new message
117
+ * also bumps its folder's badge at once and is announced to `onMessageCreated`, and the server's `Folder` update events (the
118
+ * folder's real counts) are applied to the badges - see `useFolderCounts()`.
119
+ *
120
+ * Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
121
+ * Renders nothing and does nothing where there is no window (server-side rendering).
122
+ */
123
+ export function useMailLiveUpdates({
124
+ userUid,
125
+ mailboxes,
126
+ mailboxFolders,
127
+ onFolderCreated,
128
+ onFolderChanged,
129
+ onFolderDeleted,
130
+ onFoldersListed,
131
+ onMessageCreated,
132
+ onSendEvent,
133
+ }: UseMailLiveUpdatesOptions): MailLiveUpdates {
134
+ const [live, setLive] = useState<LiveUpdates>(NO_LIVE_UPDATES);
135
+ const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
136
+ const folderCounts = useFolderCounts(mailboxes, folders, onFoldersListed);
137
+ // The latest inputs, for the long-lived listeners below.
138
+ const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts });
139
+ latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts };
140
+
141
+ const channels = useMemo(() => pushChannelsFor(mailboxFolders, mailboxes), [mailboxFolders, mailboxes]);
142
+ const channelKey = channels.join("|");
143
+ const channelsRef = useRef(channels);
144
+ channelsRef.current = channels;
145
+
146
+ useEffect(() => {
147
+ if (!userUid) {
148
+ return;
149
+ }
150
+ const client = getPushClient();
151
+ let stopped = false;
152
+ let timer: ReturnType<typeof setTimeout> | undefined;
153
+ let pendingFolders = new Set<string>();
154
+ let pendingUnknown = false;
155
+ let everOpen = false;
156
+
157
+ // Never runs once `stopped`: whatever sets it clears the timer first.
158
+ function flush() {
159
+ timer = undefined;
160
+ const folderUids = pendingUnknown ? null : new Set(pendingFolders);
161
+ pendingFolders = new Set();
162
+ pendingUnknown = false;
163
+ setLive((previous) => ({ tick: previous.tick + 1, folderUids }));
164
+ latestRef.current.folderCounts.refresh(0);
165
+ }
166
+
167
+ /** Asks for a refresh: of the given folders, or (no argument) of whatever may have changed. */
168
+ function schedule(folderUid?: string) {
169
+ if (stopped) {
170
+ return;
171
+ }
172
+ if (folderUid) {
173
+ pendingFolders.add(folderUid);
174
+ } else {
175
+ pendingUnknown = true;
176
+ }
177
+ timer ??= setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS);
178
+ }
179
+
180
+ const offEvent = client.onEvent((event) => {
181
+ const sendEvent = parseSendEvent(event);
182
+ if (sendEvent) {
183
+ // 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.
184
+ latestRef.current.onSendEvent?.(sendEvent);
185
+ schedule();
186
+ } else if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
187
+ if (event.action === "create" && isMessage(event.data)) {
188
+ // Once per message, whatever else the event triggers: the badge first, then whoever announces it.
189
+ if (latestRef.current.folderCounts.noteCreated(event.data)) {
190
+ latestRef.current.onMessageCreated?.(event.data);
191
+ }
192
+ }
193
+ schedule(folderOfMessageEvent(event));
194
+ } else if (FOLDER_EVENT.test(event.type) && event.action === "create" && isFolder(event.data)) {
195
+ // 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
196
+ // channel list follows the tree), and its real counts read - all without a reload.
197
+ const folder = event.data;
198
+ const known = latestRef.current.mailboxFolders.some((entry) => entry.folders.some((f) => f.uid === folder.uid));
199
+ if (!known) {
200
+ latestRef.current.onFolderCreated(folder);
201
+ latestRef.current.folderCounts.refresh(0);
202
+ }
203
+ } else if (FOLDER_EVENT.test(event.type) && event.action === "update") {
204
+ if (isFolderChange(event.data)) {
205
+ latestRef.current.onFolderChanged?.(event.data);
206
+ }
207
+ latestRef.current.folderCounts.applyFolderEvent(event.data);
208
+ } else if (FOLDER_EVENT.test(event.type) && event.action === "delete") {
209
+ const uid = (event.data as { uid?: unknown } | null | undefined)?.uid;
210
+ if (typeof uid === "string") {
211
+ latestRef.current.onFolderDeleted?.(uid);
212
+ }
213
+ }
214
+ });
215
+ // Anything published while the socket was down is gone for good, so every reconnect is followed by a refresh.
216
+ const offStatus = client.onStatus((status) => {
217
+ if (status === "open") {
218
+ if (everOpen) {
219
+ schedule();
220
+ }
221
+ everOpen = true;
222
+ }
223
+ });
224
+
225
+ const poll = setInterval(() => {
226
+ if (document.visibilityState === "visible") {
227
+ schedule();
228
+ }
229
+ }, LIVE_POLL_INTERVAL_MS);
230
+ function refreshNow() {
231
+ if (document.visibilityState === "visible") {
232
+ client.reconnectNow();
233
+ schedule();
234
+ }
235
+ }
236
+ function handleOnline() {
237
+ client.reconnectNow();
238
+ schedule();
239
+ }
240
+ document.addEventListener("visibilitychange", refreshNow);
241
+ window.addEventListener("focus", refreshNow);
242
+ window.addEventListener("online", handleOnline);
243
+
244
+ // A sign-out, in this tab or another, ended the session behind the socket: close it for good and stop refreshing.
245
+ const signOut = typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel(SIGN_OUT_CHANNEL);
246
+ signOut?.addEventListener("message", (event: MessageEvent<{ type?: string }>) => {
247
+ if (event.data?.type === "sign-out") {
248
+ stopped = true;
249
+ clearTimeout(timer);
250
+ client.close();
251
+ }
252
+ });
253
+
254
+ client.setChannels(channelsRef.current);
255
+ client.start();
256
+
257
+ return () => {
258
+ stopped = true;
259
+ clearTimeout(timer);
260
+ clearInterval(poll);
261
+ offEvent();
262
+ offStatus();
263
+ document.removeEventListener("visibilitychange", refreshNow);
264
+ window.removeEventListener("focus", refreshNow);
265
+ window.removeEventListener("online", handleOnline);
266
+ signOut?.close();
267
+ };
268
+ }, [userUid]);
269
+
270
+ useEffect(() => {
271
+ if (userUid) {
272
+ getPushClient().setChannels(channels);
273
+ }
274
+ }, [userUid, channelKey]);
275
+
276
+ return { live, folderCounts };
277
+ }
@@ -0,0 +1,60 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+ import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import { getMyMailboxAccess } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
8
+
9
+ /** What the server said about the caller's update access to a mailbox, remembered per mailbox for the page load. */
10
+ const answers = new Map<string, Promise<boolean | undefined>>();
11
+
12
+ /** Forgets every remembered answer - for the tests, and anything that changes the caller's access mid-session. */
13
+ export function clearMailboxUpdateAccessCache(): void {
14
+ answers.clear();
15
+ }
16
+
17
+ /** Whether the caller may change things in `mailboxUid` (move, mark and flag its messages, add its filters): the server's own ACL check. Never rejects; `undefined` when it could not be asked (which is not cached). */
18
+ function fetchUpdateAccess(mailboxUid: string): Promise<boolean | undefined> {
19
+ let answer = answers.get(mailboxUid);
20
+ if (!answer) {
21
+ answer = getMyMailboxAccess(mailboxUid).then(
22
+ (access) => access.canUpdate,
23
+ () => {
24
+ answers.delete(mailboxUid);
25
+ return undefined;
26
+ },
27
+ );
28
+ answers.set(mailboxUid, answer);
29
+ }
30
+ return answer;
31
+ }
32
+
33
+ /**
34
+ * Whether the reader may change the messages of `mailbox`: `false` only for a mailbox shared with them (`accessRole: "delegate"`) that
35
+ * the server says they may not update - a view-only share. Everything else is `true` from the first render: the reader's own mailbox
36
+ * needs no request, and a mailbox that is unknown (the shell has not listed it), or whose access could not be read, is treated as
37
+ * writable - the server still refuses what is not allowed, and hiding a usable action over a failed lookup is worse than offering one
38
+ * that then says why it failed.
39
+ */
40
+ export function useMailboxUpdateAccess(mailbox: Mailbox | undefined): boolean {
41
+ const uid = mailbox?.uid;
42
+ const delegated = mailbox?.accessRole === "delegate";
43
+ const [writable, setWritable] = useState(true);
44
+ useEffect(() => {
45
+ setWritable(true);
46
+ if (!uid || !delegated) {
47
+ return;
48
+ }
49
+ let cancelled = false;
50
+ void fetchUpdateAccess(uid).then((canUpdate) => {
51
+ if (!cancelled && canUpdate === false) {
52
+ setWritable(false);
53
+ }
54
+ });
55
+ return () => {
56
+ cancelled = true;
57
+ };
58
+ }, [uid, delegated]);
59
+ return writable;
60
+ }