@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
@@ -46,6 +46,28 @@ export interface RecipientInputProps {
46
46
  disabled?: boolean;
47
47
  /** Focuses the input when it mounts - a new message starts in To. */
48
48
  autoFocus?: boolean;
49
+
50
+ // The rest are for fields other than To/Cc/Bcc that reuse the input (the event dialog's guests); compose leaves them all out.
51
+ /** Called whenever the field's entries change: every committed entry (chips - a picked suggestion, a finished or
52
+ * pasted one, or a typed one committed by Enter or leaving the field) and the text still being typed (`""` after a
53
+ * commit). Lets a field that keeps its entries as something other than text (attendees) read them without parsing
54
+ * `value` and guessing which part of it is still being typed. */
55
+ onEdit?: (chips: string[], pending: string) => void;
56
+ /** Text being typed already when the input mounts (the last recipient of `value`), so a field that is remounted
57
+ * (a popover growing into a form) keeps it in the input instead of turning it into a chip. */
58
+ initialPending?: string;
59
+ /** Chips this returns true for are not drawn (they stay in the value, and out of the suggestions): a field that
60
+ * lists some entries itself. Backspace in an empty input removes the last chip that is drawn. */
61
+ hideChip?: (chip: string) => boolean;
62
+ placeholder?: string;
63
+ /** The input's accessible name, for a field without a `<label htmlFor>`. */
64
+ ariaLabel?: string;
65
+ /** The chip list's accessible name; `${label} recipients` by default. */
66
+ listLabel?: string;
67
+ /** Classes for the field's outer element, after its own (a border, padding). */
68
+ className?: string;
69
+ /** The suggestions' `z-index`, for a field inside an overlay that stacks above the default's (`z-[60]`). */
70
+ dropdownZIndex?: number;
49
71
  }
50
72
 
51
73
  interface DropdownPosition {
@@ -92,8 +114,16 @@ export default function RecipientInput({
92
114
  debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS,
93
115
  disabled,
94
116
  autoFocus,
117
+ onEdit,
118
+ initialPending = "",
119
+ hideChip,
120
+ placeholder,
121
+ ariaLabel,
122
+ listLabel,
123
+ className,
124
+ dropdownZIndex,
95
125
  }: RecipientInputProps) {
96
- const [pending, setPending] = useState("");
126
+ const [pending, setPending] = useState(initialPending);
97
127
  const [suggestions, setSuggestions] = useState<RecipientSuggestion[]>([]);
98
128
  const [open, setOpen] = useState(false);
99
129
  const [activeIndex, setActiveIndex] = useState(0);
@@ -110,6 +140,8 @@ export default function RecipientInput({
110
140
  const chips = pendingActive ? tokens.slice(0, -1) : tokens;
111
141
  const inputValue = pendingActive ? pending : "";
112
142
  const query = pendingActive ? pendingText : "";
143
+ const drawn = chips.map((chip, index) => ({ chip, index })).filter(({ chip }) => !hideChip?.(chip));
144
+ const lastDrawn = drawn.length > 0 ? drawn[drawn.length - 1].index : -1;
113
145
  const listboxId = `${id}-suggestions`;
114
146
  const optionId = (index: number) => `${id}-suggestion-${index}`;
115
147
 
@@ -174,6 +206,7 @@ export default function RecipientInput({
174
206
 
175
207
  function emit(nextChips: string[], nextPending: string) {
176
208
  onChange([...nextChips, nextPending.trim()].filter((part) => part.length > 0).join(", "));
209
+ onEdit?.(nextChips, nextPending.trim());
177
210
  }
178
211
 
179
212
  function handleInputChange(e: ChangeEvent<HTMLInputElement>) {
@@ -185,18 +218,23 @@ export default function RecipientInput({
185
218
  }
186
219
 
187
220
  function commitPending(): string {
188
- const next = [...chips, pendingText].filter((part) => part.length > 0).join(", ");
221
+ const committed = [...chips, pendingText].filter((part) => part.length > 0);
189
222
  setPending("");
190
223
  setOpen(false);
191
- return next;
224
+ if (pendingText) {
225
+ onEdit?.(committed, "");
226
+ }
227
+ return committed.join(", ");
192
228
  }
193
229
 
194
230
  function select(suggestion: RecipientSuggestion) {
195
- const next = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })].join(", ");
231
+ const committed = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })];
232
+ const next = committed.join(", ");
196
233
  setPending("");
197
234
  setOpen(false);
198
235
  setSuggestions([]);
199
236
  onChange(next);
237
+ onEdit?.(committed, "");
200
238
  onCommit?.(next);
201
239
  }
202
240
 
@@ -249,9 +287,12 @@ export default function RecipientInput({
249
287
  }
250
288
  return;
251
289
  case "Backspace":
252
- if (inputValue === "" && chips.length > 0) {
290
+ if (inputValue === "" && lastDrawn >= 0) {
253
291
  e.preventDefault();
254
- emit(chips.slice(0, -1), "");
292
+ emit(
293
+ chips.filter((_, i) => i !== lastDrawn),
294
+ "",
295
+ );
255
296
  }
256
297
  return;
257
298
  }
@@ -263,10 +304,10 @@ export default function RecipientInput({
263
304
  }
264
305
 
265
306
  return (
266
- <div ref={anchorRef} className="flex-1 min-w-0 flex flex-wrap items-center gap-1">
267
- {chips.length > 0 && (
268
- <ul role="list" aria-label={`${label} recipients`} className="contents">
269
- {chips.map((chip, index) => {
307
+ <div ref={anchorRef} className={`flex-1 min-w-0 flex flex-wrap items-center gap-1${className ? ` ${className}` : ""}`}>
308
+ {drawn.length > 0 && (
309
+ <ul role="list" aria-label={listLabel ?? `${label} recipients`} className="contents">
310
+ {drawn.map(({ chip, index }) => {
270
311
  const recipient = parseRecipient(chip);
271
312
  const valid = isValidRecipientAddress(recipient.address);
272
313
  const name = recipient.displayName || recipient.address;
@@ -303,6 +344,8 @@ export default function RecipientInput({
303
344
  role="combobox"
304
345
  autoComplete="off"
305
346
  aria-autocomplete="list"
347
+ aria-label={ariaLabel}
348
+ placeholder={placeholder}
306
349
  aria-expanded={expanded}
307
350
  aria-controls={listboxId}
308
351
  aria-activedescendant={expanded ? optionId(active) : undefined}
@@ -325,7 +368,7 @@ export default function RecipientInput({
325
368
  role="listbox"
326
369
  aria-label={`${label} suggestions`}
327
370
  onMouseDown={(e) => e.preventDefault()}
328
- style={{ position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight }}
371
+ style={{ position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight, zIndex: dropdownZIndex }}
329
372
  className="z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1"
330
373
  >
331
374
  {visible.map((suggestion, index) => (
@@ -1,147 +1,147 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useEffect, useRef, useState } from "react";
6
- import { EditorContent, useEditor } from "@tiptap/react";
7
- import StarterKit from "@tiptap/starter-kit";
8
- import { TextStyleKit } from "@tiptap/extension-text-style";
9
- import TextAlign from "@tiptap/extension-text-align";
10
- import Highlight from "@tiptap/extension-highlight";
11
- import Image from "@tiptap/extension-image";
12
- import { TableKit } from "@tiptap/extension-table";
13
- import Placeholder from "@tiptap/extension-placeholder";
14
- import ComposeToolbar from "./ComposeToolbar.js";
15
-
16
- export interface RichTextEditorProps {
17
- value: string;
18
- onChange: (value: string) => void;
19
- height?: string;
20
- /** When true, the editor fills its parent's height via flexbox instead of a fixed `height` — for
21
- * embedding inside a flex-column container of variable height (the floating Compose window) where
22
- * a literal pixel height can't be computed up front. `height` is ignored when this is set. */
23
- fill?: boolean;
24
- /** Passed straight through to `ComposeToolbar` — see its own doc comment on this prop. */
25
- onUploadImage: (file: File) => Promise<string | null>;
26
- /** Focuses the editor with the caret at the very start of the document once it mounts - a reply or forward, whose
27
- * body starts with an empty paragraph above the signature and quote, so typing goes above them. Only read when
28
- * the editor is created. */
29
- autoFocusStart?: boolean;
30
- /** Called once the editor is ready, with `value` as the editor itself serializes it: TipTap drops what its schema
31
- * doesn't hold (e.g. a blockquote's `style`) and adds an empty paragraph after a trailing non-paragraph node (a
32
- * quote). That normalization happens on the editor's first transaction - even one that only moves the caret -
33
- * and isn't reported through `onChange`, so a caller can compare later edits against this instead of `value`. */
34
- onInitialized?: (value: string) => void;
35
- /**
36
- * HTML to add at the end of the document, once, as soon as there is some (it may be undefined when the editor is created and
37
- * defined later). A reply opens its editor before the quoted original has been fetched and hands the quote in through this when
38
- * it arrives - so whatever has been typed above stays, and the quote lands where it always did, under the signature.
39
- */
40
- appendHtml?: string;
41
- /** Called with the document as the editor serializes it, right after `appendHtml` was added. */
42
- onAppended?: (value: string) => void;
43
- }
44
-
45
- /**
46
- * A WYSIWYG rich-text editor for the Compose window's message body — replaces `MonacoHtmlEditor` (an HTML
47
- * *source* editor) with a real Outlook-Home-tab-style formatting experience, built on TipTap/ProseMirror.
48
- * Used by `ComposeWindow` (the floating overlay, not a dedicated page — see `ComposeContext.tsx`'s doc
49
- * comment).
50
- *
51
- * `value` seeds the editor's initial content only — like `MonacoHtmlEditor`, this never re-syncs from a
52
- * later `value` prop change; TipTap owns its own document once created. Live edits flow out as HTML (via
53
- * `editor.getHTML()`) through `onChange`.
54
- *
55
- * `immediatelyRender: false` is required for this framework's SSR: the page embedding `ComposeWindow` is
56
- * rendered server-side under plain Node before hydration (see `ReactRoute`), and TipTap's default
57
- * (`immediatelyRender: true`) tries to mount its ProseMirror view during that very first render, which
58
- * both can't work without a real DOM and produces a hydration mismatch. With it `false`, `useEditor`
59
- * returns `null` until the editor actually mounts client-side after hydration — `ComposeToolbar`/
60
- * `EditorContent` both already handle a `null` editor by rendering their disabled/empty state, so this
61
- * needs no extra loading-placeholder logic here. Unlike `MonacoHtmlEditor`, no dynamic `import()` is
62
- * needed to defer loading TipTap's modules off the SSR path — `immediatelyRender: false` alone is
63
- * sufficient here since TipTap (unlike Monaco) never touches the DOM at module-evaluation time, only when
64
- * an editor view actually mounts.
65
- */
66
- export default function RichTextEditor({
67
- value,
68
- onChange,
69
- height = "360px",
70
- fill = false,
71
- onUploadImage,
72
- autoFocusStart = false,
73
- onInitialized,
74
- appendHtml,
75
- onAppended,
76
- }: RichTextEditorProps) {
77
- const onChangeRef = useRef(onChange);
78
- onChangeRef.current = onChange;
79
- const onInitializedRef = useRef(onInitialized);
80
- onInitializedRef.current = onInitialized;
81
- const onAppendedRef = useRef(onAppended);
82
- onAppendedRef.current = onAppended;
83
- const appendedRef = useRef<string | undefined>(undefined);
84
- // Transactions before `create` (the autofocus, and the normalizing one below) aren't edits.
85
- const initializedRef = useRef(false);
86
- // Held for this editor's lifetime: TipTap focuses a tick after creating the editor, and a re-render passing a
87
- // different value in between would replace the option (via `setOptions`) before it's read.
88
- const [autofocus] = useState<"start" | false>(autoFocusStart ? "start" : false);
89
-
90
- const editor = useEditor({
91
- extensions: [
92
- StarterKit.configure({ link: { openOnClick: false } }),
93
- TextStyleKit.configure({ backgroundColor: false, lineHeight: false }),
94
- TextAlign.configure({ types: ["heading", "paragraph"] }),
95
- Highlight,
96
- Image,
97
- TableKit.configure({ table: { resizable: true } }),
98
- Placeholder.configure({ placeholder: "Write your message…" }),
99
- ],
100
- content: value,
101
- autofocus,
102
- immediatelyRender: false,
103
- editorProps: {
104
- // Ctrl/Cmd+Enter is the compose window's Send. TipTap binds it to a hard break, and a key it handles never reaches the
105
- // window's shortcut (which skips what a widget already took), so it inserted a line break and sent nothing. Returning true here
106
- // stops ProseMirror handling it without preventing the event, which goes on to the shortcut. Shift+Enter is still the hard break.
107
- handleDOMEvents: { keydown: (_view, event) => event.key === "Enter" && (event.ctrlKey || event.metaKey) },
108
- },
109
- onCreate: ({ editor: created }) => {
110
- // An empty transaction runs the schema's append-transaction normalization now, before any edit.
111
- created.view.dispatch(created.state.tr);
112
- initializedRef.current = true;
113
- onInitializedRef.current?.(created.getHTML());
114
- },
115
- onUpdate: ({ editor: updated }) => {
116
- if (initializedRef.current) {
117
- onChangeRef.current(updated.getHTML());
118
- }
119
- },
120
- });
121
-
122
- // Added once, at the end, whatever the reader has typed meanwhile (the caret stays where it is).
123
- useEffect(() => {
124
- if (!editor || appendHtml === undefined || appendedRef.current === appendHtml) {
125
- return;
126
- }
127
- appendedRef.current = appendHtml;
128
- editor.commands.insertContentAt(editor.state.doc.content.size, appendHtml);
129
- onAppendedRef.current?.(editor.getHTML());
130
- }, [editor, appendHtml]);
131
-
132
- return (
133
- <div className={["border border-border rounded-sm overflow-hidden", fill ? "h-full flex flex-col" : ""].filter(Boolean).join(" ")}>
134
- <ComposeToolbar editor={editor} onUploadImage={onUploadImage} />
135
- <EditorContent
136
- editor={editor}
137
- style={fill ? undefined : { height }}
138
- className={[
139
- "overflow-y-auto px-3 py-2 text-sm [&_.tiptap]:outline-none [&_.tiptap]:h-full",
140
- fill ? "flex-1 min-h-0" : "",
141
- ]
142
- .filter(Boolean)
143
- .join(" ")}
144
- />
145
- </div>
146
- );
147
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useEffect, useRef, useState } from "react";
6
+ import { EditorContent, useEditor } from "@tiptap/react";
7
+ import StarterKit from "@tiptap/starter-kit";
8
+ import { TextStyleKit } from "@tiptap/extension-text-style";
9
+ import TextAlign from "@tiptap/extension-text-align";
10
+ import Highlight from "@tiptap/extension-highlight";
11
+ import Image from "@tiptap/extension-image";
12
+ import { TableKit } from "@tiptap/extension-table";
13
+ import Placeholder from "@tiptap/extension-placeholder";
14
+ import ComposeToolbar from "./ComposeToolbar.js";
15
+
16
+ export interface RichTextEditorProps {
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ height?: string;
20
+ /** When true, the editor fills its parent's height via flexbox instead of a fixed `height` — for
21
+ * embedding inside a flex-column container of variable height (the floating Compose window) where
22
+ * a literal pixel height can't be computed up front. `height` is ignored when this is set. */
23
+ fill?: boolean;
24
+ /** Passed straight through to `ComposeToolbar` — see its own doc comment on this prop. */
25
+ onUploadImage: (file: File) => Promise<string | null>;
26
+ /** Focuses the editor with the caret at the very start of the document once it mounts - a reply or forward, whose
27
+ * body starts with an empty paragraph above the signature and quote, so typing goes above them. Only read when
28
+ * the editor is created. */
29
+ autoFocusStart?: boolean;
30
+ /** Called once the editor is ready, with `value` as the editor itself serializes it: TipTap drops what its schema
31
+ * doesn't hold (e.g. a blockquote's `style`) and adds an empty paragraph after a trailing non-paragraph node (a
32
+ * quote). That normalization happens on the editor's first transaction - even one that only moves the caret -
33
+ * and isn't reported through `onChange`, so a caller can compare later edits against this instead of `value`. */
34
+ onInitialized?: (value: string) => void;
35
+ /**
36
+ * HTML to add at the end of the document, once, as soon as there is some (it may be undefined when the editor is created and
37
+ * defined later). A reply opens its editor before the quoted original has been fetched and hands the quote in through this when
38
+ * it arrives - so whatever has been typed above stays, and the quote lands where it always did, under the signature.
39
+ */
40
+ appendHtml?: string;
41
+ /** Called with the document as the editor serializes it, right after `appendHtml` was added. */
42
+ onAppended?: (value: string) => void;
43
+ }
44
+
45
+ /**
46
+ * A WYSIWYG rich-text editor for the Compose window's message body — replaces `MonacoHtmlEditor` (an HTML
47
+ * *source* editor) with a real Outlook-Home-tab-style formatting experience, built on TipTap/ProseMirror.
48
+ * Used by `ComposeWindow` (the floating overlay, not a dedicated page — see `ComposeContext.tsx`'s doc
49
+ * comment).
50
+ *
51
+ * `value` seeds the editor's initial content only — like `MonacoHtmlEditor`, this never re-syncs from a
52
+ * later `value` prop change; TipTap owns its own document once created. Live edits flow out as HTML (via
53
+ * `editor.getHTML()`) through `onChange`.
54
+ *
55
+ * `immediatelyRender: false` is required for this framework's SSR: the page embedding `ComposeWindow` is
56
+ * rendered server-side under plain Node before hydration (see `ReactRoute`), and TipTap's default
57
+ * (`immediatelyRender: true`) tries to mount its ProseMirror view during that very first render, which
58
+ * both can't work without a real DOM and produces a hydration mismatch. With it `false`, `useEditor`
59
+ * returns `null` until the editor actually mounts client-side after hydration — `ComposeToolbar`/
60
+ * `EditorContent` both already handle a `null` editor by rendering their disabled/empty state, so this
61
+ * needs no extra loading-placeholder logic here. Unlike `MonacoHtmlEditor`, no dynamic `import()` is
62
+ * needed to defer loading TipTap's modules off the SSR path — `immediatelyRender: false` alone is
63
+ * sufficient here since TipTap (unlike Monaco) never touches the DOM at module-evaluation time, only when
64
+ * an editor view actually mounts.
65
+ */
66
+ export default function RichTextEditor({
67
+ value,
68
+ onChange,
69
+ height = "360px",
70
+ fill = false,
71
+ onUploadImage,
72
+ autoFocusStart = false,
73
+ onInitialized,
74
+ appendHtml,
75
+ onAppended,
76
+ }: RichTextEditorProps) {
77
+ const onChangeRef = useRef(onChange);
78
+ onChangeRef.current = onChange;
79
+ const onInitializedRef = useRef(onInitialized);
80
+ onInitializedRef.current = onInitialized;
81
+ const onAppendedRef = useRef(onAppended);
82
+ onAppendedRef.current = onAppended;
83
+ const appendedRef = useRef<string | undefined>(undefined);
84
+ // Transactions before `create` (the autofocus, and the normalizing one below) aren't edits.
85
+ const initializedRef = useRef(false);
86
+ // Held for this editor's lifetime: TipTap focuses a tick after creating the editor, and a re-render passing a
87
+ // different value in between would replace the option (via `setOptions`) before it's read.
88
+ const [autofocus] = useState<"start" | false>(autoFocusStart ? "start" : false);
89
+
90
+ const editor = useEditor({
91
+ extensions: [
92
+ StarterKit.configure({ link: { openOnClick: false } }),
93
+ TextStyleKit.configure({ backgroundColor: false, lineHeight: false }),
94
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
95
+ Highlight,
96
+ Image,
97
+ TableKit.configure({ table: { resizable: true } }),
98
+ Placeholder.configure({ placeholder: "Write your message…" }),
99
+ ],
100
+ content: value,
101
+ autofocus,
102
+ immediatelyRender: false,
103
+ editorProps: {
104
+ // Ctrl/Cmd+Enter is the compose window's Send. TipTap binds it to a hard break, and a key it handles never reaches the
105
+ // window's shortcut (which skips what a widget already took), so it inserted a line break and sent nothing. Returning true here
106
+ // stops ProseMirror handling it without preventing the event, which goes on to the shortcut. Shift+Enter is still the hard break.
107
+ handleDOMEvents: { keydown: (_view, event) => event.key === "Enter" && (event.ctrlKey || event.metaKey) },
108
+ },
109
+ onCreate: ({ editor: created }) => {
110
+ // An empty transaction runs the schema's append-transaction normalization now, before any edit.
111
+ created.view.dispatch(created.state.tr);
112
+ initializedRef.current = true;
113
+ onInitializedRef.current?.(created.getHTML());
114
+ },
115
+ onUpdate: ({ editor: updated }) => {
116
+ if (initializedRef.current) {
117
+ onChangeRef.current(updated.getHTML());
118
+ }
119
+ },
120
+ });
121
+
122
+ // Added once, at the end, whatever the reader has typed meanwhile (the caret stays where it is).
123
+ useEffect(() => {
124
+ if (!editor || appendHtml === undefined || appendedRef.current === appendHtml) {
125
+ return;
126
+ }
127
+ appendedRef.current = appendHtml;
128
+ editor.commands.insertContentAt(editor.state.doc.content.size, appendHtml);
129
+ onAppendedRef.current?.(editor.getHTML());
130
+ }, [editor, appendHtml]);
131
+
132
+ return (
133
+ <div className={["border border-border rounded-sm overflow-hidden", fill ? "h-full flex flex-col" : ""].filter(Boolean).join(" ")}>
134
+ <ComposeToolbar editor={editor} onUploadImage={onUploadImage} />
135
+ <EditorContent
136
+ editor={editor}
137
+ style={fill ? undefined : { height }}
138
+ className={[
139
+ "overflow-y-auto px-3 py-2 text-sm [&_.tiptap]:outline-none [&_.tiptap]:h-full",
140
+ fill ? "flex-1 min-h-0" : "",
141
+ ]
142
+ .filter(Boolean)
143
+ .join(" ")}
144
+ />
145
+ </div>
146
+ );
147
+ }
@@ -1,60 +1,60 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
-
6
- /** Saves a compose window's unsaved edits (or waits for a save already on the wire), resolving to `false` when
7
- * something the window holds is still unsaved afterwards (any other value counts as saved). Never rejects. */
8
- export type ComposeFlush = () => Promise<unknown>;
9
-
10
- const flushers = new Set<ComposeFlush>();
11
- let signingOut = false;
12
-
13
- /**
14
- * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
15
- * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
16
- * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
17
- * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
18
- */
19
- export function markSigningOut(): void {
20
- signingOut = true;
21
- }
22
-
23
- /** Undoes `markSigningOut()`. */
24
- export function clearSigningOut(): void {
25
- signingOut = false;
26
- }
27
-
28
- /** Whether `markSigningOut()` has been called (and not cleared). */
29
- export function isSigningOut(): boolean {
30
- return signingOut;
31
- }
32
-
33
- /**
34
- * Registers an open compose window's draft flush, so leaving the app on purpose (Sign Out) can save the
35
- * last edits still waiting on the autosave debounce before the session ends. Returns the unregister
36
- * function. Module-level rather than React context: `AppShell` (the sign-out handler) renders the
37
- * `ComposeProvider` itself, so it can't read that provider's context.
38
- */
39
- export function registerComposeFlush(flush: ComposeFlush): () => void {
40
- flushers.add(flush);
41
- return () => {
42
- flushers.delete(flush);
43
- };
44
- }
45
-
46
- /**
47
- * Runs every registered flush and waits for all of them, but no longer than `timeoutMs`. Resolves to `true`
48
- * only when every window reported its content saved in time (a window whose save failed also shows the user
49
- * why, with Discard / Keep editing). Never rejects.
50
- */
51
- export async function flushComposeDrafts(timeoutMs: number): Promise<boolean> {
52
- let timer: ReturnType<typeof setTimeout> | undefined;
53
- const timeout = new Promise<boolean>((resolve) => {
54
- timer = setTimeout(() => resolve(false), timeoutMs);
55
- });
56
- const all = Promise.all([...flushers].map((flush) => flush().catch(() => false))).then((results) => results.every((result) => result !== false));
57
- const saved = await Promise.race([all, timeout]);
58
- clearTimeout(timer);
59
- return saved;
60
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+
6
+ /** Saves a compose window's unsaved edits (or waits for a save already on the wire), resolving to `false` when
7
+ * something the window holds is still unsaved afterwards (any other value counts as saved). Never rejects. */
8
+ export type ComposeFlush = () => Promise<unknown>;
9
+
10
+ const flushers = new Set<ComposeFlush>();
11
+ let signingOut = false;
12
+
13
+ /**
14
+ * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
15
+ * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
16
+ * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
17
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
18
+ */
19
+ export function markSigningOut(): void {
20
+ signingOut = true;
21
+ }
22
+
23
+ /** Undoes `markSigningOut()`. */
24
+ export function clearSigningOut(): void {
25
+ signingOut = false;
26
+ }
27
+
28
+ /** Whether `markSigningOut()` has been called (and not cleared). */
29
+ export function isSigningOut(): boolean {
30
+ return signingOut;
31
+ }
32
+
33
+ /**
34
+ * Registers an open compose window's draft flush, so leaving the app on purpose (Sign Out) can save the
35
+ * last edits still waiting on the autosave debounce before the session ends. Returns the unregister
36
+ * function. Module-level rather than React context: `AppShell` (the sign-out handler) renders the
37
+ * `ComposeProvider` itself, so it can't read that provider's context.
38
+ */
39
+ export function registerComposeFlush(flush: ComposeFlush): () => void {
40
+ flushers.add(flush);
41
+ return () => {
42
+ flushers.delete(flush);
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Runs every registered flush and waits for all of them, but no longer than `timeoutMs`. Resolves to `true`
48
+ * only when every window reported its content saved in time (a window whose save failed also shows the user
49
+ * why, with Discard / Keep editing). Never rejects.
50
+ */
51
+ export async function flushComposeDrafts(timeoutMs: number): Promise<boolean> {
52
+ let timer: ReturnType<typeof setTimeout> | undefined;
53
+ const timeout = new Promise<boolean>((resolve) => {
54
+ timer = setTimeout(() => resolve(false), timeoutMs);
55
+ });
56
+ const all = Promise.all([...flushers].map((flush) => flush().catch(() => false))).then((results) => results.every((result) => result !== false));
57
+ const saved = await Promise.race([all, timeout]);
58
+ clearTimeout(timer);
59
+ return saved;
60
+ }