@rapidmx/web-client 0.14.1 → 0.16.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 (314) hide show
  1. package/README.md +373 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/domains/[uid].tsx +164 -163
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -348
  6. package/apps/admin/index.tsx +127 -124
  7. package/apps/admin/mailboxes/[uid].tsx +269 -224
  8. package/apps/admin/mailboxes/new/index.tsx +28 -28
  9. package/apps/admin/plugins/index.tsx +15 -15
  10. package/apps/admin/retention-policy/index.tsx +39 -39
  11. package/apps/admin/signing-certificates/index.tsx +343 -343
  12. package/apps/escrow/audit-log/index.tsx +196 -196
  13. package/apps/escrow/matters/[uid].tsx +621 -621
  14. package/apps/shared/appearance/AppearanceForm.tsx +9 -1
  15. package/apps/shared/auth/adminAccess.ts +99 -99
  16. package/apps/shared/calendar/bookingPlugin.ts +100 -0
  17. package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
  18. package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
  19. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  21. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  23. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  24. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +53 -4
  25. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
  27. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  28. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  29. package/apps/shared/components/admin/setup/SetupWizard.tsx +444 -444
  30. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  31. package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
  32. package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
  33. package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
  34. package/apps/shared/components/calendar/EventDetails.tsx +401 -0
  35. package/apps/shared/components/calendar/EventEditor.tsx +635 -0
  36. package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
  37. package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
  38. package/apps/shared/components/calendar/EventModal.tsx +206 -946
  39. package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
  40. package/apps/shared/components/calendar/EventShell.tsx +202 -0
  41. package/apps/shared/components/calendar/FindATime.tsx +379 -0
  42. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  43. package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
  44. package/apps/shared/components/calendar/MonthView.tsx +185 -173
  45. package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
  46. package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
  47. package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
  48. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
  49. package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
  50. package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
  51. package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
  52. package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
  53. package/apps/shared/components/calendar/allDay.ts +124 -124
  54. package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
  55. package/apps/shared/components/calendar/eventForm.ts +108 -0
  56. package/apps/shared/components/calendar/eventFormat.ts +210 -0
  57. package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
  58. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  59. package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
  60. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  61. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  62. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  63. package/apps/shared/components/contacts/layout/ContactsShell.tsx +217 -216
  64. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  65. package/apps/shared/components/layout/AppShell.tsx +480 -480
  66. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  67. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  68. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  69. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  70. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  71. package/apps/shared/components/layout/UserMenu.tsx +439 -372
  72. package/apps/shared/components/mail/ConversationList.tsx +323 -271
  73. package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
  74. package/apps/shared/components/mail/InviteCard.tsx +387 -0
  75. package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
  76. package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
  77. package/apps/shared/components/mail/SwipeRow.tsx +120 -0
  78. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  79. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  80. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1673
  81. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  82. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  83. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  84. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  85. package/apps/shared/components/mail/invite/InviteRowChip.tsx +104 -0
  86. package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
  87. package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
  88. package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
  89. package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
  90. package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
  91. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  92. package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
  93. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  94. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  95. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  96. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  97. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +255 -253
  99. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
  101. package/apps/shared/gestures/useEnterSlide.ts +54 -0
  102. package/apps/shared/gestures/useSwipe.ts +114 -0
  103. package/apps/shared/gestures/useSwipeSlide.ts +113 -0
  104. package/apps/shared/gestures/useWheelPaging.ts +107 -0
  105. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  106. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  107. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  108. package/apps/shared/keyboard/dispatch.ts +124 -124
  109. package/apps/shared/keyboard/format.ts +89 -89
  110. package/apps/shared/keyboard/keymap.ts +114 -114
  111. package/apps/shared/keyboard/registry.ts +65 -65
  112. package/apps/shared/keyboard/targets.ts +79 -79
  113. package/apps/shared/mail/folderCounts.ts +17 -0
  114. package/apps/shared/mail/folderTree.ts +143 -143
  115. package/apps/shared/mail/listAllPages.ts +39 -39
  116. package/apps/shared/mail/newMailNotifications.ts +3 -15
  117. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  118. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  119. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  120. package/apps/shared/mail/primaryMailbox.ts +71 -0
  121. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  122. package/apps/shared/mail/useMailConnection.ts +205 -203
  123. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  124. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  125. package/apps/shared/mail/useNewMailNotifications.ts +2 -2
  126. package/apps/shared/navigation/AppRouter.tsx +300 -300
  127. package/apps/shared/navigation/routerContext.tsx +83 -83
  128. package/apps/shared/notifications/preferences.ts +36 -0
  129. package/apps/shared/notifications/store.ts +9 -0
  130. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  131. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  132. package/apps/shared/search/localIndexBuilder.ts +481 -481
  133. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  134. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  135. package/apps/shared/signing/enrollmentView.ts +251 -251
  136. package/apps/shared/signing/useNow.ts +19 -19
  137. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  138. package/apps/shared/styles/app.css +396 -386
  139. package/apps/www/_routes.ts +1 -0
  140. package/apps/www/calendar/index.tsx +578 -469
  141. package/apps/www/contacts/[uid].tsx +109 -109
  142. package/apps/www/contacts/index.tsx +6 -0
  143. package/apps/www/index.tsx +2847 -2332
  144. package/apps/www/messages/[uid].tsx +135 -108
  145. package/apps/www/settings/auto-reply/index.tsx +133 -133
  146. package/apps/www/settings/encryption/index.tsx +1287 -1287
  147. package/apps/www/settings/filters/[uid].tsx +176 -176
  148. package/apps/www/settings/filters/index.tsx +102 -102
  149. package/apps/www/settings/filters/new/index.tsx +140 -140
  150. package/apps/www/settings/labels/index.tsx +204 -204
  151. package/apps/www/settings/privacy/index.tsx +492 -492
  152. package/apps/www/settings/profile/index.tsx +248 -0
  153. package/apps/www/settings/read-receipts/index.tsx +147 -147
  154. package/apps/www/settings/sharing/index.tsx +256 -256
  155. package/apps/www/settings/signatures/[uid].tsx +172 -172
  156. package/apps/www/settings/signatures/index.tsx +88 -88
  157. package/apps/www/settings/signatures/new/index.tsx +135 -135
  158. package/apps/www/tasks/index.tsx +649 -649
  159. package/dist/apps/admin/data-requests/index.js +1 -1
  160. package/dist/apps/admin/domains/[uid].js +4 -2
  161. package/dist/apps/admin/index.js +2 -1
  162. package/dist/apps/admin/mailboxes/[uid].js +12 -2
  163. package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
  164. package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
  165. package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
  166. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
  167. package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
  168. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  169. package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -3
  170. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  171. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  172. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  173. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  174. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +28 -4
  176. package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
  177. package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
  178. package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
  180. package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
  181. package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
  182. package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
  183. package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
  184. package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
  185. package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
  186. package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
  187. package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
  188. package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
  189. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
  190. package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
  191. package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
  192. package/dist/apps/shared/components/calendar/EventModal.js +57 -459
  193. package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
  194. package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
  195. package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
  196. package/dist/apps/shared/components/calendar/EventShell.js +134 -0
  197. package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
  198. package/dist/apps/shared/components/calendar/FindATime.js +190 -0
  199. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  200. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  201. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.d.ts +8 -0
  202. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
  203. package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
  204. package/dist/apps/shared/components/calendar/MonthView.js +11 -5
  205. package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
  206. package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
  207. package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
  208. package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
  209. package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
  210. package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
  211. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  212. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
  213. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
  214. package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
  215. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
  216. package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
  217. package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
  218. package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
  219. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
  220. package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
  221. package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
  222. package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
  223. package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
  224. package/dist/apps/shared/components/calendar/eventForm.js +1 -0
  225. package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
  226. package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
  227. package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
  228. package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
  229. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
  230. package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
  231. package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
  232. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  233. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  234. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  235. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -3
  236. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  237. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  238. package/dist/apps/shared/components/layout/UserMenu.d.ts +14 -9
  239. package/dist/apps/shared/components/layout/UserMenu.js +74 -26
  240. package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
  241. package/dist/apps/shared/components/mail/ConversationList.js +21 -4
  242. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
  243. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  244. package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
  245. package/dist/apps/shared/components/mail/InviteCard.js +150 -0
  246. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
  247. package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
  248. package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
  249. package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
  250. package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
  251. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
  252. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  253. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  254. package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +23 -0
  255. package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
  256. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
  257. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
  258. package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
  259. package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
  260. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
  261. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
  262. package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
  263. package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
  264. package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
  265. package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
  266. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  267. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  268. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +10 -0
  269. package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
  270. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  271. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  272. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  273. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  274. package/dist/apps/shared/components/settings/layout/SettingsShell.js +7 -5
  275. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  276. package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
  277. package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
  278. package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
  279. package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
  280. package/dist/apps/shared/gestures/useSwipe.js +90 -0
  281. package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
  282. package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
  283. package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
  284. package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
  285. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  286. package/dist/apps/shared/mail/folderCounts.js +12 -0
  287. package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
  288. package/dist/apps/shared/mail/newMailNotifications.js +3 -12
  289. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  290. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  291. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  292. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  293. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  294. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  295. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  296. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  297. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  298. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  299. package/dist/apps/shared/mail/useNewMailNotifications.js +3 -2
  300. package/dist/apps/shared/notifications/preferences.d.ts +13 -0
  301. package/dist/apps/shared/notifications/preferences.js +36 -0
  302. package/dist/apps/shared/notifications/store.d.ts +1 -0
  303. package/dist/apps/shared/notifications/store.js +7 -23
  304. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  305. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  306. package/dist/apps/shared/styles/app.css +396 -386
  307. package/dist/apps/www/_routes.js +1 -0
  308. package/dist/apps/www/calendar/index.js +78 -7
  309. package/dist/apps/www/contacts/index.js +3 -1
  310. package/dist/apps/www/index.js +548 -210
  311. package/dist/apps/www/messages/[uid].js +13 -5
  312. package/dist/apps/www/settings/profile/index.d.ts +4 -0
  313. package/dist/apps/www/settings/profile/index.js +124 -0
  314. package/package.json +2 -2
@@ -1,143 +1,143 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
6
- import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
7
-
8
- /**
9
- * The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
10
- * for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
11
- */
12
- export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
13
-
14
- /**
15
- * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
16
- * Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
17
- * to just the mail ones itself, or those other apps' folders leak into its sidebar.
18
- */
19
- export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
20
-
21
- /** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
22
- export const SEND_FOLDER_TYPES = ["outbox", "sent_items"] as const;
23
-
24
- export function isMailFolder(folder: { type: string }): boolean {
25
- return MAIL_FOLDER_TYPES.has(folder.type);
26
- }
27
-
28
- function sortKey(type: string): number {
29
- const index = FOLDER_ORDER.indexOf(type);
30
- return index === -1 ? FOLDER_ORDER.length : index;
31
- }
32
-
33
- /** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
34
- export function sortFolders(folders: Folder[]): Folder[] {
35
- return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
36
- }
37
-
38
- /** What a sidebar row is: a folder that exists, or (`placeholder`) one that is expected to but has not arrived yet. */
39
- export type FolderRow = { kind: "folder"; folder: Folder } | { kind: "placeholder"; type: string };
40
-
41
- /**
42
- * The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
43
- * server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
44
- * arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
45
- */
46
- export function folderRows(folders: Folder[], sending: number): FolderRow[] {
47
- const rows: FolderRow[] = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
48
- if (sending > 0) {
49
- for (const type of SEND_FOLDER_TYPES) {
50
- if (!folders.some((folder) => folder.type === type)) {
51
- rows.push({ kind: "placeholder", type });
52
- }
53
- }
54
- rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
55
- }
56
- return rows;
57
- }
58
-
59
- function rowType(row: FolderRow): string {
60
- return row.kind === "folder" ? row.folder.type : row.type;
61
- }
62
-
63
- /** What a folder can be changed to by an event or a refetch: everything about it except the counts, which the counts overlay owns. */
64
- export type FolderChange = Partial<Pick<Folder, "name" | "type" | "parentFolderUid" | "color">> & { uid: string; mailboxUid?: string };
65
-
66
- const CHANGEABLE = ["name", "type", "parentFolderUid", "color"] as const;
67
-
68
- /**
69
- * Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
70
- * colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
71
- * nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
72
- */
73
- export function upsertFolder(entries: MailboxFolders[], folder: Folder | FolderChange): MailboxFolders[] {
74
- let changed = false;
75
- const next = entries.map((entry) => {
76
- const existing = entry.folders.find((f) => f.uid === folder.uid);
77
- if (!existing) {
78
- // Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
79
- if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
80
- return entry;
81
- }
82
- // A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
83
- // not know it existed) is not a second row: the older of the two is the one shown.
84
- const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
85
- if (sameType) {
86
- if (!isOlder(folder, sameType)) {
87
- return entry;
88
- }
89
- changed = true;
90
- return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
91
- }
92
- changed = true;
93
- return { ...entry, folders: [...entry.folders, folder] };
94
- }
95
- const patch: Partial<Folder> = {};
96
- for (const key of CHANGEABLE) {
97
- if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
98
- (patch as Record<string, unknown>)[key] = folder[key];
99
- }
100
- }
101
- if (Object.keys(patch).length === 0) {
102
- return entry;
103
- }
104
- changed = true;
105
- const updated = { ...existing, ...patch };
106
- return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
107
- });
108
- return changed ? next : entries;
109
- }
110
-
111
- function isFullFolder(folder: Folder | FolderChange): folder is Folder {
112
- return typeof folder.name === "string" && typeof folder.type === "string";
113
- }
114
-
115
- /** Whether `a` was created before `b`: only when both say when. */
116
- function isOlder(a: Folder, b: Folder): boolean {
117
- // (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
118
- const first = Date.parse(a.dateCreated);
119
- const second = Date.parse(b.dateCreated);
120
- return Number.isFinite(first) && Number.isFinite(second) && first < second;
121
- }
122
-
123
- /** Takes a deleted folder out of the tree. The same array when it was not in it. */
124
- export function removeFolder(entries: MailboxFolders[], folderUid: string): MailboxFolders[] {
125
- if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
126
- return entries;
127
- }
128
- return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
129
- }
130
-
131
- /**
132
- * Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
133
- * are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
134
- * again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
135
- */
136
- export function reconcileFolders(entries: MailboxFolders[], listed: Folder[]): MailboxFolders[] {
137
- return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
138
- }
139
-
140
- /** Every folder uid in the tree. */
141
- export function knownFolderUids(entries: MailboxFolders[]): Set<string> {
142
- return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
143
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
6
+ import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
7
+
8
+ /**
9
+ * The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
10
+ * for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
11
+ */
12
+ export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
13
+
14
+ /**
15
+ * A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
16
+ * Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
17
+ * to just the mail ones itself, or those other apps' folders leak into its sidebar.
18
+ */
19
+ export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
20
+
21
+ /** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
22
+ export const SEND_FOLDER_TYPES = ["outbox", "sent_items"] as const;
23
+
24
+ export function isMailFolder(folder: { type: string }): boolean {
25
+ return MAIL_FOLDER_TYPES.has(folder.type);
26
+ }
27
+
28
+ function sortKey(type: string): number {
29
+ const index = FOLDER_ORDER.indexOf(type);
30
+ return index === -1 ? FOLDER_ORDER.length : index;
31
+ }
32
+
33
+ /** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
34
+ export function sortFolders(folders: Folder[]): Folder[] {
35
+ return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
36
+ }
37
+
38
+ /** What a sidebar row is: a folder that exists, or (`placeholder`) one that is expected to but has not arrived yet. */
39
+ export type FolderRow = { kind: "folder"; folder: Folder } | { kind: "placeholder"; type: string };
40
+
41
+ /**
42
+ * The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
43
+ * server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
44
+ * arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
45
+ */
46
+ export function folderRows(folders: Folder[], sending: number): FolderRow[] {
47
+ const rows: FolderRow[] = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
48
+ if (sending > 0) {
49
+ for (const type of SEND_FOLDER_TYPES) {
50
+ if (!folders.some((folder) => folder.type === type)) {
51
+ rows.push({ kind: "placeholder", type });
52
+ }
53
+ }
54
+ rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
55
+ }
56
+ return rows;
57
+ }
58
+
59
+ function rowType(row: FolderRow): string {
60
+ return row.kind === "folder" ? row.folder.type : row.type;
61
+ }
62
+
63
+ /** What a folder can be changed to by an event or a refetch: everything about it except the counts, which the counts overlay owns. */
64
+ export type FolderChange = Partial<Pick<Folder, "name" | "type" | "parentFolderUid" | "color">> & { uid: string; mailboxUid?: string };
65
+
66
+ const CHANGEABLE = ["name", "type", "parentFolderUid", "color"] as const;
67
+
68
+ /**
69
+ * Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
70
+ * colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
71
+ * nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
72
+ */
73
+ export function upsertFolder(entries: MailboxFolders[], folder: Folder | FolderChange): MailboxFolders[] {
74
+ let changed = false;
75
+ const next = entries.map((entry) => {
76
+ const existing = entry.folders.find((f) => f.uid === folder.uid);
77
+ if (!existing) {
78
+ // Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
79
+ if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
80
+ return entry;
81
+ }
82
+ // A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
83
+ // not know it existed) is not a second row: the older of the two is the one shown.
84
+ const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
85
+ if (sameType) {
86
+ if (!isOlder(folder, sameType)) {
87
+ return entry;
88
+ }
89
+ changed = true;
90
+ return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
91
+ }
92
+ changed = true;
93
+ return { ...entry, folders: [...entry.folders, folder] };
94
+ }
95
+ const patch: Partial<Folder> = {};
96
+ for (const key of CHANGEABLE) {
97
+ if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
98
+ (patch as Record<string, unknown>)[key] = folder[key];
99
+ }
100
+ }
101
+ if (Object.keys(patch).length === 0) {
102
+ return entry;
103
+ }
104
+ changed = true;
105
+ const updated = { ...existing, ...patch };
106
+ return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
107
+ });
108
+ return changed ? next : entries;
109
+ }
110
+
111
+ function isFullFolder(folder: Folder | FolderChange): folder is Folder {
112
+ return typeof folder.name === "string" && typeof folder.type === "string";
113
+ }
114
+
115
+ /** Whether `a` was created before `b`: only when both say when. */
116
+ function isOlder(a: Folder, b: Folder): boolean {
117
+ // (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
118
+ const first = Date.parse(a.dateCreated);
119
+ const second = Date.parse(b.dateCreated);
120
+ return Number.isFinite(first) && Number.isFinite(second) && first < second;
121
+ }
122
+
123
+ /** Takes a deleted folder out of the tree. The same array when it was not in it. */
124
+ export function removeFolder(entries: MailboxFolders[], folderUid: string): MailboxFolders[] {
125
+ if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
126
+ return entries;
127
+ }
128
+ return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
129
+ }
130
+
131
+ /**
132
+ * Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
133
+ * are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
134
+ * again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
135
+ */
136
+ export function reconcileFolders(entries: MailboxFolders[], listed: Folder[]): MailboxFolders[] {
137
+ return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
138
+ }
139
+
140
+ /** Every folder uid in the tree. */
141
+ export function knownFolderUids(entries: MailboxFolders[]): Set<string> {
142
+ return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
143
+ }
@@ -1,39 +1,39 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
-
6
- /** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
7
- export const LIST_PAGE_SIZE = 500;
8
-
9
- /** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
10
- * page) from looping forever. 40 pages x 500 = 20,000 items. */
11
- export const MAX_LIST_PAGES = 40;
12
-
13
- export interface ListAllPagesResult<T> {
14
- items: T[];
15
- /** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
16
- * the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
17
- truncated: boolean;
18
- }
19
-
20
- /**
21
- * Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
22
- * (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
23
- * rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
24
- */
25
- export async function listAllPages<T>(
26
- fetchPage: (page: number) => Promise<T[]>,
27
- pageSize: number = LIST_PAGE_SIZE,
28
- maxPages: number = MAX_LIST_PAGES,
29
- ): Promise<ListAllPagesResult<T>> {
30
- const items: T[] = [];
31
- for (let page = 0; page < maxPages; page++) {
32
- const batch = await fetchPage(page);
33
- items.push(...batch);
34
- if (batch.length < pageSize) {
35
- return { items, truncated: false };
36
- }
37
- }
38
- return { items, truncated: true };
39
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+
6
+ /** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
7
+ export const LIST_PAGE_SIZE = 500;
8
+
9
+ /** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
10
+ * page) from looping forever. 40 pages x 500 = 20,000 items. */
11
+ export const MAX_LIST_PAGES = 40;
12
+
13
+ export interface ListAllPagesResult<T> {
14
+ items: T[];
15
+ /** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
16
+ * the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
17
+ truncated: boolean;
18
+ }
19
+
20
+ /**
21
+ * Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
22
+ * (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
23
+ * rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
24
+ */
25
+ export async function listAllPages<T>(
26
+ fetchPage: (page: number) => Promise<T[]>,
27
+ pageSize: number = LIST_PAGE_SIZE,
28
+ maxPages: number = MAX_LIST_PAGES,
29
+ ): Promise<ListAllPagesResult<T>> {
30
+ const items: T[] = [];
31
+ for (let page = 0; page < maxPages; page++) {
32
+ const batch = await fetchPage(page);
33
+ items.push(...batch);
34
+ if (batch.length < pageSize) {
35
+ return { items, truncated: false };
36
+ }
37
+ }
38
+ return { items, truncated: true };
39
+ }
@@ -6,17 +6,14 @@ import type { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailAp
6
6
  import { formatMailAddress, splitMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
7
7
 
8
8
  /**
9
- * What decides whether a new message is announced, what the announcement says, and the two settings behind it (both per
10
- * browser, in `localStorage`): whether new-mail pop-ups are on at all, and what the user told the "turn on desktop
11
- * notifications" offer.
9
+ * What decides whether a new message is announced, what the announcement says, and the setting behind it (per browser, in
10
+ * `localStorage`): what the user told the "turn on desktop notifications" offer. Whether pop-ups are on at all is the
11
+ * notifications switch (`notifications/preferences.ts`).
12
12
  *
13
13
  * The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
14
14
  * ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
15
15
  */
16
16
 
17
- /** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
18
- export const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
19
-
20
17
  /** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
21
18
  export const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
22
19
 
@@ -49,15 +46,6 @@ function writeStorage(key: string, value: string | null): void {
49
46
  }
50
47
  }
51
48
 
52
- /** Whether new-mail pop-ups are on (the default). */
53
- export function getNewMailPopupsEnabled(): boolean {
54
- return readStorage(NEW_MAIL_POPUPS_KEY) !== "off";
55
- }
56
-
57
- export function setNewMailPopupsEnabled(enabled: boolean): void {
58
- writeStorage(NEW_MAIL_POPUPS_KEY, enabled ? null : "off");
59
- }
60
-
61
49
  /** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
62
50
  export function getDesktopOfferDismissed(): boolean {
63
51
  return readStorage(DESKTOP_OFFER_KEY) !== null;
@@ -0,0 +1,192 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useSyncExternalStore } from "react";
6
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import type { MailAddressLike } from "@rapidmx/react-shared/mail/mailAddress.js";
8
+ import type { NotificationAction } from "../../notifications/store.js";
9
+ import type { SendRequest } from "./sendJob.js";
10
+
11
+ /**
12
+ * A reply (or forward) this tab has sent, as the open conversation shows it until the server's own copy is in the thread.
13
+ *
14
+ * This is a *view* of the background send that is already there, not a second state machine: `startSend()` (`sendJob.ts`) opens an entry, and the
15
+ * places that already report how a send went close it - the client-side failure (`notifyFailure()`), the server's own outcome (`handleSendEvent()`
16
+ * in `sendOutcomes.ts`: `send-succeeded`, `send-failed`) and a synchronous send. The Outbox indicator, the pop-ups and this entry therefore agree,
17
+ * because they are all told by the same code at the same moment.
18
+ *
19
+ * Why the message has to be shown from here for a while: a message the server accepted is in Outbox, which has no `conversationId` yet - it is
20
+ * resolved when the message is relayed and filed in Sent Items (`ScheduledSendJob`), keeping the same `uid`. Until then no fetch of the
21
+ * conversation can return it, so the thread pane draws it from what was composed.
22
+ *
23
+ * `sending` covers everything up to the server's answer (assembling, the request, the queue in Outbox); `failed` is a send that did not go and is
24
+ * the sender's to resolve (`actions` are the very ones the pop-up offers: Retry, Open draft ...); `sent` is a message the server has relayed and
25
+ * that is waiting for the thread to be read again to find its Sent Items copy.
26
+ */
27
+ export type OutgoingState = "sending" | "failed" | "sent";
28
+
29
+ export interface OutgoingReply {
30
+ /** The draft's uid - which is also the uid of the message in Outbox and of its Sent Items copy, and so what the real message replaces this by. */
31
+ uid: string;
32
+ mailboxUid: string;
33
+ /** What the message replies to (`Message.inReplyTo` / `references`): how the pane knows which conversation it belongs to. */
34
+ inReplyTo?: string;
35
+ references: string[];
36
+ sender: MailAddressLike;
37
+ to: MailAddressLike[];
38
+ cc: MailAddressLike[];
39
+ bcc: MailAddressLike[];
40
+ subject: string;
41
+ /** The composed body, as the editor wrote it. */
42
+ html: string;
43
+ startedAt: number;
44
+ state: OutgoingState;
45
+ /** `failed`: why. */
46
+ failure?: string;
47
+ /** `failed`: what the sender can do about it. */
48
+ actions: NotificationAction[];
49
+ /** `sent`: how many times the thread was read without finding the message - see `settleOutgoing()`. */
50
+ misses: number;
51
+ }
52
+
53
+ /** The most entries kept; the oldest go first. Every entry is a message this tab sent, so this is generous. */
54
+ const MAX_TRACKED = 25;
55
+ /** A sent message that a read of its conversation has not found this many times is not part of that conversation (the server filed it elsewhere). */
56
+ export const MAX_SETTLE_MISSES = 2;
57
+
58
+ const NO_REPLIES: readonly OutgoingReply[] = [];
59
+ let replies: OutgoingReply[] = [];
60
+ let snapshot: readonly OutgoingReply[] = replies;
61
+ const listeners = new Set<() => void>();
62
+
63
+ function commit(next: OutgoingReply[]): void {
64
+ replies = next;
65
+ snapshot = next;
66
+ for (const listener of [...listeners]) {
67
+ listener();
68
+ }
69
+ }
70
+
71
+ function update(uid: string, patch: Partial<OutgoingReply>): void {
72
+ if (replies.some((reply) => reply.uid === uid)) {
73
+ commit(replies.map((reply) => (reply.uid === uid ? { ...reply, ...patch } : reply)));
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Starts showing `request`'s message in the conversation it replies to - or does nothing for a message that replies to nothing (a new message
79
+ * starts a conversation of its own, it is never part of an open one) and for a send-later message (it is not sent yet). Called again for the same
80
+ * message - a Retry - it starts over as `sending`.
81
+ *
82
+ * The thread is `request.threading` (the compose session's), else what the draft itself records: a draft opened again from "Open draft" is a
83
+ * session with no threading of its own, but the server kept it on the message.
84
+ */
85
+ export function trackOutgoing(request: SendRequest): void {
86
+ const source = request.threading ?? request.draft;
87
+ const references = source.references ?? [];
88
+ if (request.scheduledSendTime || (!source.inReplyTo && references.length === 0)) {
89
+ return;
90
+ }
91
+ const { mailbox } = request;
92
+ const reply: OutgoingReply = {
93
+ uid: request.draft.uid,
94
+ mailboxUid: request.mailboxUid,
95
+ inReplyTo: source.inReplyTo,
96
+ references,
97
+ // The mailbox is only unknown when the window's own load of it had not succeeded: "Me" is what a sender is called then.
98
+ sender: mailbox ? { address: mailbox.primarySmtpAddress, displayName: mailbox.displayName } : { address: "Me" },
99
+ to: request.to,
100
+ cc: request.cc,
101
+ bcc: request.bcc,
102
+ subject: request.subject,
103
+ html: request.html,
104
+ startedAt: Date.now(),
105
+ state: "sending",
106
+ actions: [],
107
+ misses: 0,
108
+ };
109
+ commit([...replies.filter((existing) => existing.uid !== reply.uid), reply].slice(-MAX_TRACKED));
110
+ }
111
+
112
+ /** The send did not go: `failure` is why, `actions` what can be done (Retry, Open draft ...). */
113
+ export function failOutgoing(uid: string, failure: string, actions: NotificationAction[]): void {
114
+ update(uid, { state: "failed", failure, actions });
115
+ }
116
+
117
+ /** A failed send is being tried again. */
118
+ export function markOutgoingSending(uid: string): void {
119
+ update(uid, { state: "sending", failure: undefined, actions: [] });
120
+ }
121
+
122
+ /** The server has relayed the message: its Sent Items copy is what the conversation shows next. */
123
+ export function markOutgoingSent(uid: string): void {
124
+ update(uid, { state: "sent" });
125
+ }
126
+
127
+ /** Stops showing a message - it went back to Drafts to be edited. */
128
+ export function forgetOutgoing(uid: string): void {
129
+ if (replies.some((reply) => reply.uid === uid)) {
130
+ commit(replies.filter((reply) => reply.uid !== uid));
131
+ }
132
+ }
133
+
134
+ /** Whether `reply` continues the conversation `messages` are: it answers one of them, or its `References` chain runs through one. */
135
+ export function belongsToThread(reply: OutgoingReply, messages: readonly Pick<Message, "messageId">[]): boolean {
136
+ return messages.some((message) => !!message.messageId && (message.messageId === reply.inReplyTo || reply.references.includes(message.messageId)));
137
+ }
138
+
139
+ /** The uids of the messages sent from here whose real copy is in `messages`, the thread as it was just read. */
140
+ export function adoptedOutgoing(mailboxUid: string, messages: readonly Message[]): string[] {
141
+ return replies.filter((reply) => reply.mailboxUid === mailboxUid && messages.some((message) => message.uid === reply.uid)).map((reply) => reply.uid);
142
+ }
143
+
144
+ /**
145
+ * Called with a mailbox's conversation as it was just read, `messages` being the whole thread: forgets the messages whose real copy is now in it
146
+ * (`adoptedOutgoing()`), and gives up on a *sent* message that belongs to this thread but that the read did not find `MAX_SETTLE_MISSES` times in a
147
+ * row - the server filed it in a conversation of its own, and it is not to be shown here. A message of another conversation is left alone.
148
+ *
149
+ * A pane draws the messages it has read *before* it calls this, so that there is never a moment with neither the pending card nor the real one.
150
+ */
151
+ export function settleOutgoing(mailboxUid: string, messages: readonly Message[]): void {
152
+ const adopted = new Set(adoptedOutgoing(mailboxUid, messages));
153
+ const next: OutgoingReply[] = [];
154
+ let changed = adopted.size > 0;
155
+ for (const reply of replies) {
156
+ if (adopted.has(reply.uid)) {
157
+ continue;
158
+ }
159
+ if (reply.mailboxUid === mailboxUid && reply.state === "sent" && belongsToThread(reply, messages)) {
160
+ changed = true;
161
+ if (reply.misses + 1 < MAX_SETTLE_MISSES) {
162
+ next.push({ ...reply, misses: reply.misses + 1 });
163
+ }
164
+ } else {
165
+ next.push(reply);
166
+ }
167
+ }
168
+ if (changed) {
169
+ commit(next);
170
+ }
171
+ }
172
+
173
+ export function getOutgoingReplies(): readonly OutgoingReply[] {
174
+ return snapshot;
175
+ }
176
+
177
+ function subscribe(listener: () => void): () => void {
178
+ listeners.add(listener);
179
+ return () => {
180
+ listeners.delete(listener);
181
+ };
182
+ }
183
+
184
+ /** The messages this tab has sent that a conversation may still need to show, oldest first. */
185
+ export function useOutgoingReplies(): readonly OutgoingReply[] {
186
+ return useSyncExternalStore(subscribe, getOutgoingReplies, () => NO_REPLIES);
187
+ }
188
+
189
+ /** Forgets everything - for a test. */
190
+ export function resetOutgoingReplies(): void {
191
+ commit([]);
192
+ }