@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,312 +1,312 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { PropsWithChildren, createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
6
- import { DraftThreading } from "@rapidmx/react-shared/mail/mailApi.js";
7
- import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
8
- import { ResumeCompose, registerComposeOpener } from "../../../mail/outbox/composeBridge.js";
9
- import { markComposePhase } from "./composePerf.js";
10
- import ComposeWindowPlaceholder from "./ComposeWindowPlaceholder.js";
11
-
12
- /** The CSS variable (on `<html>`) that holds how far down the screen the open compose windows start, in px. */
13
- export const COMPOSE_TOP_VAR = "--rr-compose-top";
14
-
15
- type ComposeWindowComponent = typeof import("./ComposeWindow.js").default;
16
-
17
- let loadedComposeWindow: ComposeWindowComponent | undefined;
18
- let loadingComposeWindow: Promise<ComposeWindowComponent> | undefined;
19
-
20
- /**
21
- * The compose window - with the rich-text editor (TipTap/ProseMirror), the recipient inputs and the send pipeline - is
22
- * the largest piece of the mail app and most page loads never open it, so it is a separate chunk, loaded here once. Not
23
- * `React.lazy()`: a chunk that failed to download (offline, a deploy that replaced it) must be retryable, and `lazy()` keeps
24
- * the rejection for good.
25
- */
26
- function loadComposeWindow(): Promise<ComposeWindowComponent> {
27
- loadingComposeWindow ??= import("./ComposeWindow.js").then(
28
- (module) => (loadedComposeWindow = module.default),
29
- (err) => {
30
- loadingComposeWindow = undefined;
31
- throw err;
32
- },
33
- );
34
- return loadingComposeWindow;
35
- }
36
-
37
- /**
38
- * Starts downloading the compose window's code without opening one - on hover/focus of a Compose or Reply button and when
39
- * the browser is idle after the inbox has loaded - so that clicking it finds the chunk already in memory. Safe to call
40
- * any number of times, and never rejects: a failed download is retried by the click itself.
41
- */
42
- export function prefetchComposeWindow(): void {
43
- loadComposeWindow().catch(() => undefined);
44
- }
45
-
46
- /** Test seam: forgets the loaded compose window chunk, as a fresh page load would. */
47
- export function resetComposeWindowLoader(): void {
48
- loadedComposeWindow = undefined;
49
- loadingComposeWindow = undefined;
50
- }
51
-
52
- /** The compose window component once its chunk is in, else `undefined` (while loading, or `failed`), and a way to retry. */
53
- function useComposeWindowComponent(wanted: boolean): { Component: ComposeWindowComponent | undefined; failed: boolean; retry: () => void } {
54
- const [loaded, setComponent] = useState<ComposeWindowComponent | undefined>();
55
- // Also read straight from the module: a prefetch that finished after this provider mounted has no state update to
56
- // announce it, and the first click must not flash the placeholder for a chunk that is already here.
57
- const Component = loaded ?? loadedComposeWindow;
58
- const [failed, setFailed] = useState(false);
59
- const [attempt, setAttempt] = useState(0);
60
- useEffect(() => {
61
- if (!wanted || Component) {
62
- return;
63
- }
64
- let cancelled = false;
65
- setFailed(false);
66
- loadComposeWindow().then(
67
- (component) => {
68
- if (!cancelled) {
69
- setComponent(() => component);
70
- }
71
- },
72
- () => {
73
- if (!cancelled) {
74
- setFailed(true);
75
- }
76
- },
77
- );
78
- return () => {
79
- cancelled = true;
80
- };
81
- }, [wanted, Component, attempt]);
82
- return { Component, failed, retry: () => setAttempt((n) => n + 1) };
83
- }
84
-
85
- export interface ComposeSession {
86
- id: string;
87
- /** The initial sending mailbox - absent means "the caller's own mailbox" (see `ComposeWindow`). */
88
- mailboxUid?: string;
89
- initialTo?: string;
90
- initialCc?: string;
91
- initialSubject?: string;
92
- /** Pre-built HTML (already includes its own quote-attribution wrapper — see `composeQuoting.ts`)
93
- * inserted below the resolved default signature. Absent for a fresh, non-reply/forward compose. Its presence
94
- * is what makes the window open with the caret at the top of the body instead of in To. */
95
- initialQuotedHtml?: string;
96
- /** See `OpenComposeInput.encrypt`'s own doc comment. */
97
- initialEncrypt?: boolean;
98
- /** See `OpenComposeInput.threading`'s own doc comment - handed to `createDraft()` by the window. */
99
- threading?: DraftThreading;
100
- /** Which of a signature's two "default" flags to resolve against — `"new"` (the default) uses
101
- * `isDefaultForNewMessages`, `"reply_forward"` uses `isDefaultForReplyForward`. */
102
- signatureContext: "new" | "reply_forward";
103
- /** See `OpenComposeInput.suppressSigning`'s own doc comment. */
104
- suppressSigning?: boolean;
105
- /** The window opened before its quoted original was known (see `OpenComposeInput.pending`): the body waits for it. */
106
- quotePending?: boolean;
107
- /** What `OpenComposeInput.pending` resolved with, once it has - see `ComposeLateInput`. */
108
- late?: ComposeLateInput;
109
- /** See `OpenComposeInput.resume`. */
110
- resume?: ResumeCompose;
111
- minimized: boolean;
112
- }
113
-
114
- /** What a reply or forward could only work out after its window was already open (see `OpenComposeInput.pending`). */
115
- export interface ComposeLateInput {
116
- /** The quoted original - see `OpenComposeInput.quotedHtml`. */
117
- quotedHtml?: string;
118
- /** A better To than the one the window opened with - applied only if the To field is still exactly what it opened with. */
119
- to?: string;
120
- /** A better Cc, under the same rule as `to`. */
121
- cc?: string;
122
- }
123
-
124
- export interface OpenComposeInput {
125
- /** The sending mailbox to start with. Omit for a fresh message (defaults to the caller's own mailbox);
126
- * a reply/forward passes the original message's mailbox so a shared mailbox's mail replies from it.
127
- * Either way the user can change it via the compose window's From field. */
128
- mailboxUid?: string;
129
- /** Prefills the To field — e.g. Contacts' "Email" toolbar action, or Reply/Reply All/Forward. */
130
- to?: string;
131
- /** Prefills the Cc field and reveals the Cc/Bcc row — Reply All only. */
132
- cc?: string;
133
- /** Prefills the Subject field — Reply/Reply All/Forward. */
134
- subject?: string;
135
- /** See `ComposeSession.initialQuotedHtml`'s own doc comment. */
136
- quotedHtml?: string;
137
- /** See `ComposeSession.signatureContext`'s own doc comment. Defaults to `"new"` — every existing
138
- * caller (Contacts' "Email" action, the folder-sidebar "Compose" button) is a fresh compose. */
139
- signatureContext?: "new" | "reply_forward";
140
- /** `true` when the caller (`MessageDetailPane.tsx`'s Reply/Reply All) has already determined, via
141
- * `composeSecurity.ts`'s `isLikelyMailingList()`, that the message being replied to came from a
142
- * mailing list — a list that appends a footer after signing invalidates the signature (spec's own
143
- * "Mailing lists" note), so the new compose window defaults its Sign toggle off rather than on.
144
- * The user can still turn it back on manually; this only changes the *default*. */
145
- suppressSigning?: boolean;
146
- /** `true` for a reply to or forward of an encrypted message, whose quote may carry its decrypted content: the
147
- * compose window starts with "Encrypt this message" requested, so it is never autosaved as a plaintext draft and
148
- * can't be sent unencrypted without the user explicitly choosing to. */
149
- encrypt?: boolean;
150
- /** The thread this compose continues (`buildReplyThreading()` over the message being replied to or
151
- * forwarded), recorded on the draft by `createDraft()`. Without it the message is relayed with no
152
- * `In-Reply-To`/`References` at all and every mail system - the sender's own Sent Items included - files it
153
- * as a new conversation rather than part of the thread. Absent for a fresh compose, which starts one. */
154
- threading?: DraftThreading;
155
- /**
156
- * Something the window shouldn't wait for before appearing: a reply or forward opens at once from what is already in
157
- * memory (the message's own subject and sender), and this settles with what needed the network - the original's body
158
- * to quote, and for Reply All the recipients its headers name. The compose window shows meanwhile, its body area
159
- * waiting, and fills the body in when this resolves; `to`/`cc` replace the opened-with values only if the user hasn't
160
- * touched those fields. Must never reject (resolve `undefined` for "nothing more"); when set, `quotedHtml` is ignored
161
- * until it resolves.
162
- */
163
- pending?: Promise<ComposeLateInput | undefined>;
164
- /** Re-opens a message that was already composed - a failed send's "Open draft": the window continues that server draft, with every field
165
- * exactly as it was typed, instead of starting a new one. */
166
- resume?: ResumeCompose;
167
- }
168
-
169
- export interface ComposeContextValue {
170
- /** Opens a new compose window, stacked alongside any already open (Gmail allows several at once). */
171
- openCompose: (input: OpenComposeInput) => void;
172
- }
173
-
174
- const ComposeContext = createContext<ComposeContextValue>({ openCompose: () => undefined });
175
-
176
- /** Opens the floating Compose window from anywhere inside `AppShell` (any of the four webmail apps). */
177
- export function useCompose(): ComposeContextValue {
178
- return useContext(ComposeContext);
179
- }
180
-
181
- /**
182
- * Owns every currently-open Compose window and renders them stacked bottom-right, Gmail-style — see
183
- * `ComposeWindow`'s own doc comment for why this replaced the old dedicated `/compose` page. Mounted
184
- * once in `AppShell`, so every webmail app (Mail/Calendar/Contacts/Tasks) shares the same instance:
185
- * opening Compose from Contacts' "Email" action, for instance, overlays the window on top of whatever
186
- * app is currently showing, exactly like opening it from Mail's own sidebar button.
187
- */
188
- export default function ComposeProvider({ children, userUid, trusted }: PropsWithChildren<{ userUid?: string; trusted?: boolean }>) {
189
- const [sessions, setSessions] = useState<ComposeSession[]>([]);
190
- const isMobile = useIsMobile();
191
- const { Component: ComposeWindow, failed, retry: retryLoad } = useComposeWindowComponent(sessions.length > 0);
192
-
193
- // The top edge of the compose windows, published as `--rr-compose-top` so the pop-up stack (`NotificationCenter`) can keep clear of them.
194
- const windowsRef = useRef<HTMLDivElement>(null);
195
- const hasSessions = sessions.length > 0;
196
- useEffect(() => {
197
- const element = windowsRef.current;
198
- if (!element) {
199
- return;
200
- }
201
- const publish = () => document.documentElement.style.setProperty(COMPOSE_TOP_VAR, `${Math.round(element.getBoundingClientRect().top)}px`);
202
- publish();
203
- const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(publish);
204
- observer?.observe(element);
205
- window.addEventListener("resize", publish);
206
- return () => {
207
- observer?.disconnect();
208
- window.removeEventListener("resize", publish);
209
- document.documentElement.style.removeProperty(COMPOSE_TOP_VAR);
210
- };
211
- }, [hasSessions]);
212
-
213
- function openCompose({
214
- mailboxUid,
215
- to,
216
- cc,
217
- subject,
218
- quotedHtml,
219
- signatureContext = "new",
220
- suppressSigning,
221
- encrypt,
222
- threading,
223
- pending,
224
- resume,
225
- }: OpenComposeInput) {
226
- const id = crypto.randomUUID();
227
- markComposePhase(id, "click");
228
- setSessions((prev) => [
229
- ...prev,
230
- {
231
- id,
232
- mailboxUid,
233
- initialTo: to,
234
- initialCc: cc,
235
- initialSubject: subject,
236
- initialQuotedHtml: quotedHtml,
237
- initialEncrypt: encrypt,
238
- threading,
239
- signatureContext,
240
- suppressSigning,
241
- quotePending: !!pending,
242
- resume,
243
- minimized: false,
244
- },
245
- ]);
246
- // A window closed before this settles is simply not in the list any more - nothing to update.
247
- void pending
248
- ?.catch(() => undefined)
249
- .then((late) =>
250
- setSessions((prev) =>
251
- prev.map((s) =>
252
- s.id === id ? { ...s, quotePending: false, late, initialQuotedHtml: late?.quotedHtml ?? s.initialQuotedHtml } : s,
253
- ),
254
- ),
255
- );
256
- }
257
-
258
- function closeCompose(id: string) {
259
- setSessions((prev) => prev.filter((s) => s.id !== id));
260
- }
261
-
262
- function toggleMinimize(id: string) {
263
- setSessions((prev) => prev.map((s) => (s.id === id ? { ...s, minimized: !s.minimized } : s)));
264
- }
265
-
266
- const value = useMemo<ComposeContextValue>(() => ({ openCompose }), []);
267
- // Code with no React context - a failed send's "Open draft" pop-up - opens windows through this.
268
- useEffect(() => registerComposeOpener((input) => value.openCompose(input)), [value]);
269
-
270
- // On mobile, a non-minimized `ComposeWindow` renders full-screen (see that component's own doc
271
- // comment) — Gmail-style stacking of several full-screen overlays at once makes no sense there, so
272
- // at most one non-minimized session is ever shown: the most recently opened one. Minimized
273
- // sessions are small chips regardless of device, so every one of those still shows — an earlier
274
- // session becomes visible again (as its own chip, or full-screen if it's the new most-recent
275
- // non-minimized one) once whatever's currently "on top" is closed or minimized. The others stay
276
- // mounted, just hidden: unmounting them would throw away everything typed into them.
277
- const lastNonMinimizedId = isMobile ? [...sessions].reverse().find((s) => !s.minimized)?.id : undefined;
278
-
279
- return (
280
- <ComposeContext.Provider value={value}>
281
- {children}
282
- {sessions.length > 0 && (
283
- <div ref={windowsRef} className="fixed bottom-0 right-6 flex items-end gap-3 z-50">
284
- {sessions.map((session) => {
285
- const hidden = isMobile && !session.minimized && session.id !== lastNonMinimizedId;
286
- return (
287
- <div key={session.id} hidden={hidden} className={hidden ? "hidden" : "contents"}>
288
- {ComposeWindow ? (
289
- <ComposeWindow
290
- session={session}
291
- userUid={userUid}
292
- trusted={trusted}
293
- onClose={() => closeCompose(session.id)}
294
- onToggleMinimize={() => toggleMinimize(session.id)}
295
- />
296
- ) : (
297
- <ComposeWindowPlaceholder
298
- session={session}
299
- failed={failed}
300
- onRetry={retryLoad}
301
- onClose={() => closeCompose(session.id)}
302
- onToggleMinimize={() => toggleMinimize(session.id)}
303
- />
304
- )}
305
- </div>
306
- );
307
- })}
308
- </div>
309
- )}
310
- </ComposeContext.Provider>
311
- );
312
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { PropsWithChildren, createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
6
+ import { DraftThreading } from "@rapidmx/react-shared/mail/mailApi.js";
7
+ import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
8
+ import { ResumeCompose, registerComposeOpener } from "../../../mail/outbox/composeBridge.js";
9
+ import { markComposePhase } from "./composePerf.js";
10
+ import ComposeWindowPlaceholder from "./ComposeWindowPlaceholder.js";
11
+
12
+ /** The CSS variable (on `<html>`) that holds how far down the screen the open compose windows start, in px. */
13
+ export const COMPOSE_TOP_VAR = "--rr-compose-top";
14
+
15
+ type ComposeWindowComponent = typeof import("./ComposeWindow.js").default;
16
+
17
+ let loadedComposeWindow: ComposeWindowComponent | undefined;
18
+ let loadingComposeWindow: Promise<ComposeWindowComponent> | undefined;
19
+
20
+ /**
21
+ * The compose window - with the rich-text editor (TipTap/ProseMirror), the recipient inputs and the send pipeline - is
22
+ * the largest piece of the mail app and most page loads never open it, so it is a separate chunk, loaded here once. Not
23
+ * `React.lazy()`: a chunk that failed to download (offline, a deploy that replaced it) must be retryable, and `lazy()` keeps
24
+ * the rejection for good.
25
+ */
26
+ function loadComposeWindow(): Promise<ComposeWindowComponent> {
27
+ loadingComposeWindow ??= import("./ComposeWindow.js").then(
28
+ (module) => (loadedComposeWindow = module.default),
29
+ (err) => {
30
+ loadingComposeWindow = undefined;
31
+ throw err;
32
+ },
33
+ );
34
+ return loadingComposeWindow;
35
+ }
36
+
37
+ /**
38
+ * Starts downloading the compose window's code without opening one - on hover/focus of a Compose or Reply button and when
39
+ * the browser is idle after the inbox has loaded - so that clicking it finds the chunk already in memory. Safe to call
40
+ * any number of times, and never rejects: a failed download is retried by the click itself.
41
+ */
42
+ export function prefetchComposeWindow(): void {
43
+ loadComposeWindow().catch(() => undefined);
44
+ }
45
+
46
+ /** Test seam: forgets the loaded compose window chunk, as a fresh page load would. */
47
+ export function resetComposeWindowLoader(): void {
48
+ loadedComposeWindow = undefined;
49
+ loadingComposeWindow = undefined;
50
+ }
51
+
52
+ /** The compose window component once its chunk is in, else `undefined` (while loading, or `failed`), and a way to retry. */
53
+ function useComposeWindowComponent(wanted: boolean): { Component: ComposeWindowComponent | undefined; failed: boolean; retry: () => void } {
54
+ const [loaded, setComponent] = useState<ComposeWindowComponent | undefined>();
55
+ // Also read straight from the module: a prefetch that finished after this provider mounted has no state update to
56
+ // announce it, and the first click must not flash the placeholder for a chunk that is already here.
57
+ const Component = loaded ?? loadedComposeWindow;
58
+ const [failed, setFailed] = useState(false);
59
+ const [attempt, setAttempt] = useState(0);
60
+ useEffect(() => {
61
+ if (!wanted || Component) {
62
+ return;
63
+ }
64
+ let cancelled = false;
65
+ setFailed(false);
66
+ loadComposeWindow().then(
67
+ (component) => {
68
+ if (!cancelled) {
69
+ setComponent(() => component);
70
+ }
71
+ },
72
+ () => {
73
+ if (!cancelled) {
74
+ setFailed(true);
75
+ }
76
+ },
77
+ );
78
+ return () => {
79
+ cancelled = true;
80
+ };
81
+ }, [wanted, Component, attempt]);
82
+ return { Component, failed, retry: () => setAttempt((n) => n + 1) };
83
+ }
84
+
85
+ export interface ComposeSession {
86
+ id: string;
87
+ /** The initial sending mailbox - absent means "the caller's own mailbox" (see `ComposeWindow`). */
88
+ mailboxUid?: string;
89
+ initialTo?: string;
90
+ initialCc?: string;
91
+ initialSubject?: string;
92
+ /** Pre-built HTML (already includes its own quote-attribution wrapper — see `composeQuoting.ts`)
93
+ * inserted below the resolved default signature. Absent for a fresh, non-reply/forward compose. Its presence
94
+ * is what makes the window open with the caret at the top of the body instead of in To. */
95
+ initialQuotedHtml?: string;
96
+ /** See `OpenComposeInput.encrypt`'s own doc comment. */
97
+ initialEncrypt?: boolean;
98
+ /** See `OpenComposeInput.threading`'s own doc comment - handed to `createDraft()` by the window. */
99
+ threading?: DraftThreading;
100
+ /** Which of a signature's two "default" flags to resolve against — `"new"` (the default) uses
101
+ * `isDefaultForNewMessages`, `"reply_forward"` uses `isDefaultForReplyForward`. */
102
+ signatureContext: "new" | "reply_forward";
103
+ /** See `OpenComposeInput.suppressSigning`'s own doc comment. */
104
+ suppressSigning?: boolean;
105
+ /** The window opened before its quoted original was known (see `OpenComposeInput.pending`): the body waits for it. */
106
+ quotePending?: boolean;
107
+ /** What `OpenComposeInput.pending` resolved with, once it has - see `ComposeLateInput`. */
108
+ late?: ComposeLateInput;
109
+ /** See `OpenComposeInput.resume`. */
110
+ resume?: ResumeCompose;
111
+ minimized: boolean;
112
+ }
113
+
114
+ /** What a reply or forward could only work out after its window was already open (see `OpenComposeInput.pending`). */
115
+ export interface ComposeLateInput {
116
+ /** The quoted original - see `OpenComposeInput.quotedHtml`. */
117
+ quotedHtml?: string;
118
+ /** A better To than the one the window opened with - applied only if the To field is still exactly what it opened with. */
119
+ to?: string;
120
+ /** A better Cc, under the same rule as `to`. */
121
+ cc?: string;
122
+ }
123
+
124
+ export interface OpenComposeInput {
125
+ /** The sending mailbox to start with. Omit for a fresh message (defaults to the caller's own mailbox);
126
+ * a reply/forward passes the original message's mailbox so a shared mailbox's mail replies from it.
127
+ * Either way the user can change it via the compose window's From field. */
128
+ mailboxUid?: string;
129
+ /** Prefills the To field — e.g. Contacts' "Email" toolbar action, or Reply/Reply All/Forward. */
130
+ to?: string;
131
+ /** Prefills the Cc field and reveals the Cc/Bcc row — Reply All only. */
132
+ cc?: string;
133
+ /** Prefills the Subject field — Reply/Reply All/Forward. */
134
+ subject?: string;
135
+ /** See `ComposeSession.initialQuotedHtml`'s own doc comment. */
136
+ quotedHtml?: string;
137
+ /** See `ComposeSession.signatureContext`'s own doc comment. Defaults to `"new"` — every existing
138
+ * caller (Contacts' "Email" action, the folder-sidebar "Compose" button) is a fresh compose. */
139
+ signatureContext?: "new" | "reply_forward";
140
+ /** `true` when the caller (`MessageDetailPane.tsx`'s Reply/Reply All) has already determined, via
141
+ * `composeSecurity.ts`'s `isLikelyMailingList()`, that the message being replied to came from a
142
+ * mailing list — a list that appends a footer after signing invalidates the signature (spec's own
143
+ * "Mailing lists" note), so the new compose window defaults its Sign toggle off rather than on.
144
+ * The user can still turn it back on manually; this only changes the *default*. */
145
+ suppressSigning?: boolean;
146
+ /** `true` for a reply to or forward of an encrypted message, whose quote may carry its decrypted content: the
147
+ * compose window starts with "Encrypt this message" requested, so it is never autosaved as a plaintext draft and
148
+ * can't be sent unencrypted without the user explicitly choosing to. */
149
+ encrypt?: boolean;
150
+ /** The thread this compose continues (`buildReplyThreading()` over the message being replied to or
151
+ * forwarded), recorded on the draft by `createDraft()`. Without it the message is relayed with no
152
+ * `In-Reply-To`/`References` at all and every mail system - the sender's own Sent Items included - files it
153
+ * as a new conversation rather than part of the thread. Absent for a fresh compose, which starts one. */
154
+ threading?: DraftThreading;
155
+ /**
156
+ * Something the window shouldn't wait for before appearing: a reply or forward opens at once from what is already in
157
+ * memory (the message's own subject and sender), and this settles with what needed the network - the original's body
158
+ * to quote, and for Reply All the recipients its headers name. The compose window shows meanwhile, its body area
159
+ * waiting, and fills the body in when this resolves; `to`/`cc` replace the opened-with values only if the user hasn't
160
+ * touched those fields. Must never reject (resolve `undefined` for "nothing more"); when set, `quotedHtml` is ignored
161
+ * until it resolves.
162
+ */
163
+ pending?: Promise<ComposeLateInput | undefined>;
164
+ /** Re-opens a message that was already composed - a failed send's "Open draft": the window continues that server draft, with every field
165
+ * exactly as it was typed, instead of starting a new one. */
166
+ resume?: ResumeCompose;
167
+ }
168
+
169
+ export interface ComposeContextValue {
170
+ /** Opens a new compose window, stacked alongside any already open (Gmail allows several at once). */
171
+ openCompose: (input: OpenComposeInput) => void;
172
+ }
173
+
174
+ const ComposeContext = createContext<ComposeContextValue>({ openCompose: () => undefined });
175
+
176
+ /** Opens the floating Compose window from anywhere inside `AppShell` (any of the four webmail apps). */
177
+ export function useCompose(): ComposeContextValue {
178
+ return useContext(ComposeContext);
179
+ }
180
+
181
+ /**
182
+ * Owns every currently-open Compose window and renders them stacked bottom-right, Gmail-style — see
183
+ * `ComposeWindow`'s own doc comment for why this replaced the old dedicated `/compose` page. Mounted
184
+ * once in `AppShell`, so every webmail app (Mail/Calendar/Contacts/Tasks) shares the same instance:
185
+ * opening Compose from Contacts' "Email" action, for instance, overlays the window on top of whatever
186
+ * app is currently showing, exactly like opening it from Mail's own sidebar button.
187
+ */
188
+ export default function ComposeProvider({ children, userUid, trusted }: PropsWithChildren<{ userUid?: string; trusted?: boolean }>) {
189
+ const [sessions, setSessions] = useState<ComposeSession[]>([]);
190
+ const isMobile = useIsMobile();
191
+ const { Component: ComposeWindow, failed, retry: retryLoad } = useComposeWindowComponent(sessions.length > 0);
192
+
193
+ // The top edge of the compose windows, published as `--rr-compose-top` so the pop-up stack (`NotificationCenter`) can keep clear of them.
194
+ const windowsRef = useRef<HTMLDivElement>(null);
195
+ const hasSessions = sessions.length > 0;
196
+ useEffect(() => {
197
+ const element = windowsRef.current;
198
+ if (!element) {
199
+ return;
200
+ }
201
+ const publish = () => document.documentElement.style.setProperty(COMPOSE_TOP_VAR, `${Math.round(element.getBoundingClientRect().top)}px`);
202
+ publish();
203
+ const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(publish);
204
+ observer?.observe(element);
205
+ window.addEventListener("resize", publish);
206
+ return () => {
207
+ observer?.disconnect();
208
+ window.removeEventListener("resize", publish);
209
+ document.documentElement.style.removeProperty(COMPOSE_TOP_VAR);
210
+ };
211
+ }, [hasSessions]);
212
+
213
+ function openCompose({
214
+ mailboxUid,
215
+ to,
216
+ cc,
217
+ subject,
218
+ quotedHtml,
219
+ signatureContext = "new",
220
+ suppressSigning,
221
+ encrypt,
222
+ threading,
223
+ pending,
224
+ resume,
225
+ }: OpenComposeInput) {
226
+ const id = crypto.randomUUID();
227
+ markComposePhase(id, "click");
228
+ setSessions((prev) => [
229
+ ...prev,
230
+ {
231
+ id,
232
+ mailboxUid,
233
+ initialTo: to,
234
+ initialCc: cc,
235
+ initialSubject: subject,
236
+ initialQuotedHtml: quotedHtml,
237
+ initialEncrypt: encrypt,
238
+ threading,
239
+ signatureContext,
240
+ suppressSigning,
241
+ quotePending: !!pending,
242
+ resume,
243
+ minimized: false,
244
+ },
245
+ ]);
246
+ // A window closed before this settles is simply not in the list any more - nothing to update.
247
+ void pending
248
+ ?.catch(() => undefined)
249
+ .then((late) =>
250
+ setSessions((prev) =>
251
+ prev.map((s) =>
252
+ s.id === id ? { ...s, quotePending: false, late, initialQuotedHtml: late?.quotedHtml ?? s.initialQuotedHtml } : s,
253
+ ),
254
+ ),
255
+ );
256
+ }
257
+
258
+ function closeCompose(id: string) {
259
+ setSessions((prev) => prev.filter((s) => s.id !== id));
260
+ }
261
+
262
+ function toggleMinimize(id: string) {
263
+ setSessions((prev) => prev.map((s) => (s.id === id ? { ...s, minimized: !s.minimized } : s)));
264
+ }
265
+
266
+ const value = useMemo<ComposeContextValue>(() => ({ openCompose }), []);
267
+ // Code with no React context - a failed send's "Open draft" pop-up - opens windows through this.
268
+ useEffect(() => registerComposeOpener((input) => value.openCompose(input)), [value]);
269
+
270
+ // On mobile, a non-minimized `ComposeWindow` renders full-screen (see that component's own doc
271
+ // comment) — Gmail-style stacking of several full-screen overlays at once makes no sense there, so
272
+ // at most one non-minimized session is ever shown: the most recently opened one. Minimized
273
+ // sessions are small chips regardless of device, so every one of those still shows — an earlier
274
+ // session becomes visible again (as its own chip, or full-screen if it's the new most-recent
275
+ // non-minimized one) once whatever's currently "on top" is closed or minimized. The others stay
276
+ // mounted, just hidden: unmounting them would throw away everything typed into them.
277
+ const lastNonMinimizedId = isMobile ? [...sessions].reverse().find((s) => !s.minimized)?.id : undefined;
278
+
279
+ return (
280
+ <ComposeContext.Provider value={value}>
281
+ {children}
282
+ {sessions.length > 0 && (
283
+ <div ref={windowsRef} className="fixed bottom-0 right-6 flex items-end gap-3 z-50">
284
+ {sessions.map((session) => {
285
+ const hidden = isMobile && !session.minimized && session.id !== lastNonMinimizedId;
286
+ return (
287
+ <div key={session.id} hidden={hidden} className={hidden ? "hidden" : "contents"}>
288
+ {ComposeWindow ? (
289
+ <ComposeWindow
290
+ session={session}
291
+ userUid={userUid}
292
+ trusted={trusted}
293
+ onClose={() => closeCompose(session.id)}
294
+ onToggleMinimize={() => toggleMinimize(session.id)}
295
+ />
296
+ ) : (
297
+ <ComposeWindowPlaceholder
298
+ session={session}
299
+ failed={failed}
300
+ onRetry={retryLoad}
301
+ onClose={() => closeCompose(session.id)}
302
+ onToggleMinimize={() => toggleMinimize(session.id)}
303
+ />
304
+ )}
305
+ </div>
306
+ );
307
+ })}
308
+ </div>
309
+ )}
310
+ </ComposeContext.Provider>
311
+ );
312
+ }