@rapidmx/web-client 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/README.md +373 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/domains/[uid].tsx +164 -163
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -348
  6. package/apps/admin/index.tsx +127 -124
  7. package/apps/admin/mailboxes/[uid].tsx +269 -224
  8. package/apps/admin/mailboxes/new/index.tsx +28 -28
  9. package/apps/admin/plugins/index.tsx +15 -15
  10. package/apps/admin/retention-policy/index.tsx +39 -39
  11. package/apps/admin/signing-certificates/index.tsx +343 -343
  12. package/apps/escrow/audit-log/index.tsx +196 -196
  13. package/apps/escrow/matters/[uid].tsx +621 -621
  14. package/apps/shared/appearance/AppearanceForm.tsx +9 -1
  15. package/apps/shared/auth/adminAccess.ts +99 -99
  16. package/apps/shared/calendar/bookingPlugin.ts +100 -0
  17. package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
  18. package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
  19. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  21. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  23. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  24. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +53 -4
  25. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
  27. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  28. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  29. package/apps/shared/components/admin/setup/SetupWizard.tsx +444 -444
  30. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  31. package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
  32. package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
  33. package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
  34. package/apps/shared/components/calendar/EventDetails.tsx +401 -0
  35. package/apps/shared/components/calendar/EventEditor.tsx +635 -0
  36. package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
  37. package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
  38. package/apps/shared/components/calendar/EventModal.tsx +206 -946
  39. package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
  40. package/apps/shared/components/calendar/EventShell.tsx +202 -0
  41. package/apps/shared/components/calendar/FindATime.tsx +379 -0
  42. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  43. package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
  44. package/apps/shared/components/calendar/MonthView.tsx +185 -173
  45. package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
  46. package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
  47. package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
  48. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
  49. package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
  50. package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
  51. package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
  52. package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
  53. package/apps/shared/components/calendar/allDay.ts +124 -124
  54. package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
  55. package/apps/shared/components/calendar/eventForm.ts +108 -0
  56. package/apps/shared/components/calendar/eventFormat.ts +210 -0
  57. package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
  58. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  59. package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
  60. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  61. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  62. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  63. package/apps/shared/components/contacts/layout/ContactsShell.tsx +217 -216
  64. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  65. package/apps/shared/components/layout/AppShell.tsx +480 -480
  66. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  67. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  68. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  69. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  70. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  71. package/apps/shared/components/layout/UserMenu.tsx +439 -372
  72. package/apps/shared/components/mail/ConversationList.tsx +323 -271
  73. package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
  74. package/apps/shared/components/mail/InviteCard.tsx +387 -0
  75. package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
  76. package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
  77. package/apps/shared/components/mail/SwipeRow.tsx +120 -0
  78. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  79. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  80. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1673
  81. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  82. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  83. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  84. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  85. package/apps/shared/components/mail/invite/InviteRowChip.tsx +104 -0
  86. package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
  87. package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
  88. package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
  89. package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
  90. package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
  91. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  92. package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
  93. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  94. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  95. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  96. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  97. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +255 -253
  99. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
  101. package/apps/shared/gestures/useEnterSlide.ts +54 -0
  102. package/apps/shared/gestures/useSwipe.ts +114 -0
  103. package/apps/shared/gestures/useSwipeSlide.ts +113 -0
  104. package/apps/shared/gestures/useWheelPaging.ts +107 -0
  105. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  106. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  107. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  108. package/apps/shared/keyboard/dispatch.ts +124 -124
  109. package/apps/shared/keyboard/format.ts +89 -89
  110. package/apps/shared/keyboard/keymap.ts +114 -114
  111. package/apps/shared/keyboard/registry.ts +65 -65
  112. package/apps/shared/keyboard/targets.ts +79 -79
  113. package/apps/shared/mail/folderCounts.ts +17 -0
  114. package/apps/shared/mail/folderTree.ts +143 -143
  115. package/apps/shared/mail/listAllPages.ts +39 -39
  116. package/apps/shared/mail/newMailNotifications.ts +3 -15
  117. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  118. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  119. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  120. package/apps/shared/mail/primaryMailbox.ts +71 -0
  121. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  122. package/apps/shared/mail/useMailConnection.ts +205 -203
  123. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  124. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  125. package/apps/shared/mail/useNewMailNotifications.ts +2 -2
  126. package/apps/shared/navigation/AppRouter.tsx +300 -300
  127. package/apps/shared/navigation/routerContext.tsx +83 -83
  128. package/apps/shared/notifications/preferences.ts +36 -0
  129. package/apps/shared/notifications/store.ts +9 -0
  130. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  131. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  132. package/apps/shared/search/localIndexBuilder.ts +481 -481
  133. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  134. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  135. package/apps/shared/signing/enrollmentView.ts +251 -251
  136. package/apps/shared/signing/useNow.ts +19 -19
  137. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  138. package/apps/shared/styles/app.css +396 -386
  139. package/apps/www/_routes.ts +1 -0
  140. package/apps/www/calendar/index.tsx +578 -469
  141. package/apps/www/contacts/[uid].tsx +109 -109
  142. package/apps/www/contacts/index.tsx +6 -0
  143. package/apps/www/index.tsx +2847 -2332
  144. package/apps/www/messages/[uid].tsx +135 -108
  145. package/apps/www/settings/auto-reply/index.tsx +133 -133
  146. package/apps/www/settings/encryption/index.tsx +1287 -1287
  147. package/apps/www/settings/filters/[uid].tsx +176 -176
  148. package/apps/www/settings/filters/index.tsx +102 -102
  149. package/apps/www/settings/filters/new/index.tsx +140 -140
  150. package/apps/www/settings/labels/index.tsx +204 -204
  151. package/apps/www/settings/privacy/index.tsx +492 -492
  152. package/apps/www/settings/profile/index.tsx +248 -0
  153. package/apps/www/settings/read-receipts/index.tsx +147 -147
  154. package/apps/www/settings/sharing/index.tsx +256 -256
  155. package/apps/www/settings/signatures/[uid].tsx +172 -172
  156. package/apps/www/settings/signatures/index.tsx +88 -88
  157. package/apps/www/settings/signatures/new/index.tsx +135 -135
  158. package/apps/www/tasks/index.tsx +649 -649
  159. package/dist/apps/admin/data-requests/index.js +1 -1
  160. package/dist/apps/admin/domains/[uid].js +4 -2
  161. package/dist/apps/admin/index.js +2 -1
  162. package/dist/apps/admin/mailboxes/[uid].js +12 -2
  163. package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
  164. package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
  165. package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
  166. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
  167. package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
  168. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  169. package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -3
  170. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  171. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  172. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  173. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  174. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +28 -4
  176. package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
  177. package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
  178. package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
  180. package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
  181. package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
  182. package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
  183. package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
  184. package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
  185. package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
  186. package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
  187. package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
  188. package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
  189. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
  190. package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
  191. package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
  192. package/dist/apps/shared/components/calendar/EventModal.js +57 -459
  193. package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
  194. package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
  195. package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
  196. package/dist/apps/shared/components/calendar/EventShell.js +134 -0
  197. package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
  198. package/dist/apps/shared/components/calendar/FindATime.js +190 -0
  199. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  200. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  201. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.d.ts +8 -0
  202. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
  203. package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
  204. package/dist/apps/shared/components/calendar/MonthView.js +11 -5
  205. package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
  206. package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
  207. package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
  208. package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
  209. package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
  210. package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
  211. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  212. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
  213. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
  214. package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
  215. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
  216. package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
  217. package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
  218. package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
  219. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
  220. package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
  221. package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
  222. package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
  223. package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
  224. package/dist/apps/shared/components/calendar/eventForm.js +1 -0
  225. package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
  226. package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
  227. package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
  228. package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
  229. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
  230. package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
  231. package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
  232. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  233. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  234. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  235. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -3
  236. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  237. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  238. package/dist/apps/shared/components/layout/UserMenu.d.ts +14 -9
  239. package/dist/apps/shared/components/layout/UserMenu.js +74 -26
  240. package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
  241. package/dist/apps/shared/components/mail/ConversationList.js +21 -4
  242. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
  243. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  244. package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
  245. package/dist/apps/shared/components/mail/InviteCard.js +150 -0
  246. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
  247. package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
  248. package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
  249. package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
  250. package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
  251. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
  252. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  253. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  254. package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +23 -0
  255. package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
  256. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
  257. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
  258. package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
  259. package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
  260. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
  261. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
  262. package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
  263. package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
  264. package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
  265. package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
  266. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  267. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  268. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +10 -0
  269. package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
  270. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  271. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  272. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  273. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  274. package/dist/apps/shared/components/settings/layout/SettingsShell.js +7 -5
  275. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  276. package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
  277. package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
  278. package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
  279. package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
  280. package/dist/apps/shared/gestures/useSwipe.js +90 -0
  281. package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
  282. package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
  283. package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
  284. package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
  285. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  286. package/dist/apps/shared/mail/folderCounts.js +12 -0
  287. package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
  288. package/dist/apps/shared/mail/newMailNotifications.js +3 -12
  289. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  290. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  291. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  292. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  293. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  294. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  295. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  296. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  297. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  298. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  299. package/dist/apps/shared/mail/useNewMailNotifications.js +3 -2
  300. package/dist/apps/shared/notifications/preferences.d.ts +13 -0
  301. package/dist/apps/shared/notifications/preferences.js +36 -0
  302. package/dist/apps/shared/notifications/store.d.ts +1 -0
  303. package/dist/apps/shared/notifications/store.js +7 -23
  304. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  305. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  306. package/dist/apps/shared/styles/app.css +396 -386
  307. package/dist/apps/www/_routes.js +1 -0
  308. package/dist/apps/www/calendar/index.js +78 -7
  309. package/dist/apps/www/contacts/index.js +3 -1
  310. package/dist/apps/www/index.js +548 -210
  311. package/dist/apps/www/messages/[uid].js +13 -5
  312. package/dist/apps/www/settings/profile/index.d.ts +4 -0
  313. package/dist/apps/www/settings/profile/index.js +124 -0
  314. package/package.json +2 -2
@@ -0,0 +1,379 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { MouseEvent, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { format } from "date-fns";
7
+ import {
8
+ AvailabilityState,
9
+ FREE_BUSY_MAX_ADDRESSES,
10
+ PersonAvailability,
11
+ availabilityDuring,
12
+ availabilityOf,
13
+ getFreeBusy,
14
+ suggestTimes,
15
+ summarizeAvailability,
16
+ withoutOwnBlock,
17
+ } from "@rapidmx/react-shared/calendar/freeBusyApi.js";
18
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
19
+ import { addDaysToKey } from "./allDay.js";
20
+ import { EventFormController } from "./eventForm.js";
21
+ import { formatFormWhen, localDateFromKey, msToWallString, wallStringToMs } from "./eventFormat.js";
22
+ import {
23
+ SEARCH_DAYS,
24
+ WORK_END_HOUR,
25
+ WORK_START_HOUR,
26
+ clockLabel,
27
+ dayStartMs,
28
+ freeBusyErrorMessage,
29
+ isDayKey,
30
+ minuteOfDay,
31
+ summaryLine,
32
+ workingWindows,
33
+ } from "./findTimeGrid.js";
34
+
35
+ /** How long the grid waits after the last change of day or guests before asking the server, so typing a list of guests is one look-up. */
36
+ export const FIND_TIME_DEBOUNCE_MS = 300;
37
+
38
+ const HOUR_WIDTH = 52;
39
+ const NAME_WIDTH = 136;
40
+ const HEADER_HEIGHT = 28;
41
+ const ROW_HEIGHT = 44;
42
+ const TIMELINE_WIDTH = HOUR_WIDTH * 24;
43
+ const HOURS = Array.from({ length: 24 }, (_, hour) => hour);
44
+ const GRID_LINES = `repeating-linear-gradient(to right, transparent 0, transparent ${HOUR_WIDTH - 1}px, var(--color-border) ${HOUR_WIDTH - 1}px, var(--color-border) ${HOUR_WIDTH}px)`;
45
+ const HATCH = "repeating-linear-gradient(45deg, transparent 0, transparent 4px, var(--color-text-muted) 4px, var(--color-text-muted) 5px)";
46
+ const TENTATIVE_HATCH =
47
+ "repeating-linear-gradient(45deg, var(--color-danger-bg) 0, var(--color-danger-bg) 4px, var(--color-danger) 4px, var(--color-danger) 6px)";
48
+
49
+ /** "12am", "9am", "12pm" for the header cells and their labels. */
50
+ function hourLabel(hour: number): string {
51
+ return format(new Date(2000, 0, 1, hour), "haaa");
52
+ }
53
+
54
+ interface Row {
55
+ address: string;
56
+ name: string;
57
+ you: boolean;
58
+ }
59
+
60
+ type LookUp = { status: "loading" | "ready"; people: PersonAvailability[] } | { status: "error"; people: PersonAvailability[]; message: string };
61
+
62
+ /** What a row says about the proposed time, under the person's name. */
63
+ function stateText(state: AvailabilityState | undefined, status: PersonAvailability["status"] | undefined): string {
64
+ if (status === "restricted") {
65
+ return "Hides their calendar";
66
+ }
67
+ if (status === "unknown") {
68
+ return "Availability unknown";
69
+ }
70
+ if (status === undefined) {
71
+ return "Checking…";
72
+ }
73
+ // Their calendar was read, but the proposed time is not on the days it covers.
74
+ if (state === undefined) {
75
+ return "";
76
+ }
77
+ return state === "free" ? "Free" : state === "busy" ? "Busy at this time" : "Maybe busy at this time";
78
+ }
79
+
80
+ /**
81
+ * The event dialog's "Find a time" tab: a day of the guests' calendars side by side, as Google Calendar draws it. A row for you and one for each guest,
82
+ * the hours of the day across (working hours in view first), each person's busy time as a block (a tentative one hatched) and the event's proposed time as a
83
+ * highlight across every row; pressing an hour, or a place in a row, moves the event's start there (its length is kept). Under the grid: whether the proposed
84
+ * time suits everyone, and up to five suggested times - the next slots of the event's length, 8:00am to 6:00pm, when everyone who can be checked is free.
85
+ *
86
+ * Who can be checked: `POST /calendar-events/free-busy` tells a guest's busy time only for a mailbox on this server whose owner shares it with you. A guest it
87
+ * cannot see - an external address, or somebody who hides their calendar - is shown as such, in the grid and in the summary, and is never counted as free;
88
+ * suggestions ignore them (and say so). The look-up is made after a short pause (`FIND_TIME_DEBOUNCE_MS`) whenever the day or the guests change, one answer at
89
+ * a time (an answer for a look-up that has since been replaced is dropped), and a failure - too many look-ups, or no connection - says so with a retry.
90
+ */
91
+ export default function FindATime({ c }: { c: EventFormController }) {
92
+ const { values, deviceZone, organizerAddress, occurrence } = c;
93
+ const { start, end, allDay, formZone } = values;
94
+ const startDay = start.slice(0, 10);
95
+ const [dayKey, setDayKey] = useState(() => (isDayKey(startDay) ? startDay : format(new Date(), "yyyy-MM-dd")));
96
+ const [retry, setRetry] = useState(0);
97
+ // Picking another date in the form moves the grid to it.
98
+ useEffect(() => {
99
+ if (isDayKey(startDay)) {
100
+ setDayKey(startDay);
101
+ }
102
+ }, [startDay]);
103
+
104
+ // You, then each guest once (case-insensitively), at most as many as one look-up may ask about.
105
+ const rows = useMemo<Row[]>(() => {
106
+ const seen = new Set([organizerAddress.toLowerCase()]);
107
+ const guests: Row[] = [];
108
+ for (const attendee of values.attendees) {
109
+ const key = attendee.address.toLowerCase();
110
+ if (!seen.has(key)) {
111
+ seen.add(key);
112
+ guests.push({ address: attendee.address, name: attendee.displayName || attendee.address, you: false });
113
+ }
114
+ }
115
+ return [{ address: organizerAddress, name: "You", you: true }, ...guests].slice(0, FREE_BUSY_MAX_ADDRESSES);
116
+ }, [values.attendees, organizerAddress]);
117
+ const guestCount = rows.length - 1;
118
+ const addresses = rows.map((row) => row.address);
119
+ const addressesKey = addresses.map((address) => address.toLowerCase()).join(",");
120
+
121
+ const rangeStart = dayStartMs(dayKey, formZone, deviceZone);
122
+ const rangeEnd = dayStartMs(addDaysToKey(dayKey, SEARCH_DAYS), formZone, deviceZone);
123
+ const eventStart = allDay ? Number.NaN : wallStringToMs(start, formZone, deviceZone);
124
+ const eventEnd = allDay ? Number.NaN : wallStringToMs(end, formZone, deviceZone);
125
+ const timeIsValid = eventEnd > eventStart;
126
+
127
+ const [lookUp, setLookUp] = useState<LookUp>({ status: "loading", people: [] });
128
+ useEffect(() => {
129
+ if (allDay || guestCount === 0 || Number.isNaN(rangeStart) || Number.isNaN(rangeEnd)) {
130
+ return;
131
+ }
132
+ // Each run of this effect owns its answer: once the day or the guests change (or this unmounts) a slower earlier look-up is ignored.
133
+ let current = true;
134
+ setLookUp((previous) => ({ status: "loading", people: previous.people }));
135
+ const timer = setTimeout(() => {
136
+ getFreeBusy(addresses, new Date(rangeStart), new Date(rangeEnd)).then(
137
+ (response) => current && setLookUp({ status: "ready", people: availabilityOf(response) }),
138
+ (err: unknown) => current && setLookUp({ status: "error", people: [], message: freeBusyErrorMessage(err) }),
139
+ );
140
+ }, FIND_TIME_DEBOUNCE_MS);
141
+ return () => {
142
+ current = false;
143
+ clearTimeout(timer);
144
+ };
145
+ // `addresses` is what `addressesKey` says.
146
+ }, [addressesKey, rangeStart, rangeEnd, allDay, retry]);
147
+
148
+ // The event's own time, when it is being rescheduled, is on the calendars of the people already invited: not their conflict with themselves.
149
+ const people = useMemo(() => {
150
+ if (!occurrence || occurrence.allDay) {
151
+ return lookUp.people;
152
+ }
153
+ const invited = new Set([occurrence.organizer.address, ...occurrence.attendees.map((attendee) => attendee.address), organizerAddress].map((a) => a.toLowerCase()));
154
+ return withoutOwnBlock(lookUp.people, invited, Date.parse(occurrence.startDate), Date.parse(occurrence.endDate));
155
+ }, [lookUp.people, occurrence, organizerAddress]);
156
+
157
+ // Working hours scrolled into view, or the event's own start when that lies outside them.
158
+ const scroller = useRef<HTMLDivElement>(null);
159
+ useLayoutEffect(() => {
160
+ if (!scroller.current) {
161
+ return;
162
+ }
163
+ const startHour = timeIsValid && msToWallString(eventStart, formZone, deviceZone).slice(0, 10) === dayKey ? Number(msToWallString(eventStart, formZone, deviceZone).slice(11, 13)) : undefined;
164
+ const firstHour = startHour !== undefined && (startHour < WORK_START_HOUR || startHour >= WORK_END_HOUR - 3) ? Math.max(0, startHour - 1) : WORK_START_HOUR;
165
+ scroller.current.scrollLeft = firstHour * HOUR_WIDTH;
166
+ // Only a change of day moves the view: moving the event within it must not.
167
+ }, [dayKey, guestCount > 0 && !allDay]);
168
+
169
+ function startAt(minutes: number) {
170
+ c.setStart(`${dayKey}T${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`);
171
+ }
172
+
173
+ function handleTimelineClick(event: MouseEvent<HTMLDivElement>) {
174
+ const box = event.currentTarget.getBoundingClientRect();
175
+ if (box.width > 0) {
176
+ // Half hours: the smallest step of the form's own time inputs is a minute, but a row is only 52px an hour wide.
177
+ startAt(Math.min(47, Math.max(0, Math.floor(((event.clientX - box.left) / box.width) * 48))) * 30);
178
+ }
179
+ }
180
+
181
+ if (allDay) {
182
+ return <p className="text-sm text-text-muted">Finding a time works for an event with a start and end time. Uncheck All day to see when your guests are free.</p>;
183
+ }
184
+ if (guestCount === 0) {
185
+ return <p className="text-sm text-text-muted">Add guests to see when they are free.</p>;
186
+ }
187
+
188
+ const dayLabel = format(localDateFromKey(dayKey), "EEEE, MMMM d");
189
+ const proposedFrom = timeIsValid ? minuteOfDay(eventStart, dayKey, formZone, deviceZone) : 0;
190
+ const proposedTo = timeIsValid ? minuteOfDay(eventEnd, dayKey, formZone, deviceZone) : 0;
191
+ const showProposed = timeIsValid && proposedTo > proposedFrom;
192
+ const covered = timeIsValid && eventStart >= rangeStart && eventEnd <= rangeEnd;
193
+ const answered = lookUp.status === "ready";
194
+ const byAddress = new Map(people.map((person) => [person.address.toLowerCase(), person]));
195
+
196
+ const summary = answered && covered ? summarizeAvailability(people, eventStart, eventEnd) : undefined;
197
+ const suggestions =
198
+ answered && timeIsValid
199
+ ? suggestTimes(people, {
200
+ windows: workingWindows(dayKey, formZone, deviceZone),
201
+ durationMs: eventEnd - eventStart,
202
+ notBeforeMs: Date.now(),
203
+ })
204
+ : [];
205
+ const seen = people.filter((person) => person.status === "available").length;
206
+ const unseen = people.length - seen;
207
+
208
+ /** What a screen reader hears of a person whose calendar was read: their busy times on the day shown. */
209
+ function busyDescription(person: PersonAvailability): string {
210
+ const dayEnd = dayStartMs(addDaysToKey(dayKey, 1), formZone, deviceZone);
211
+ const blocks = person.busy.filter((block) => block.startMs < dayEnd && block.endMs > rangeStart);
212
+ return blocks.length === 0
213
+ ? "Free all day."
214
+ : blocks
215
+ .map((block) => `${block.tentative ? "Tentatively busy" : "Busy"} ${clockLabel(block.startMs, formZone, deviceZone)} to ${clockLabel(block.endMs, formZone, deviceZone)}`)
216
+ .join(", ");
217
+ }
218
+
219
+ return (
220
+ <div className="flex flex-col gap-3 min-w-0">
221
+ <div className="flex flex-wrap items-center gap-2">
222
+ <button
223
+ type="button"
224
+ aria-label="Previous day"
225
+ onClick={() => setDayKey((key) => addDaysToKey(key, -1))}
226
+ className="w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text"
227
+ >
228
+ &lsaquo;
229
+ </button>
230
+ <span className="text-sm font-medium min-w-[9rem] text-center">{dayLabel}</span>
231
+ <button
232
+ type="button"
233
+ aria-label="Next day"
234
+ onClick={() => setDayKey((key) => addDaysToKey(key, 1))}
235
+ className="w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text"
236
+ >
237
+ &rsaquo;
238
+ </button>
239
+ <span className="text-xs text-text-muted">{formZone.replace(/_/g, " ")}</span>
240
+ </div>
241
+
242
+ {lookUp.status === "error" && (
243
+ <div role="alert" className="flex flex-wrap items-center gap-3 text-sm text-danger">
244
+ <span>{lookUp.message}</span>
245
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setRetry((count) => count + 1)}>
246
+ Try again
247
+ </Button>
248
+ </div>
249
+ )}
250
+
251
+ <div ref={scroller} role="group" aria-label="Guests' availability" aria-busy={lookUp.status === "loading"} className="overflow-x-auto border border-border rounded-md">
252
+ <div className="relative" style={{ width: NAME_WIDTH + TIMELINE_WIDTH }}>
253
+ <div className="flex bg-surface border-b border-border" style={{ height: HEADER_HEIGHT }}>
254
+ <div className="sticky left-0 z-20 shrink-0 bg-surface border-r border-border" style={{ width: NAME_WIDTH }} />
255
+ {HOURS.map((hour) => (
256
+ <button
257
+ key={hour}
258
+ type="button"
259
+ aria-label={`Start at ${hourLabel(hour)}`}
260
+ title={`Start at ${hourLabel(hour)}`}
261
+ onClick={() => startAt(hour * 60)}
262
+ className="shrink-0 text-[10px] text-text-muted hover:bg-surface-alt hover:text-text text-left pl-1"
263
+ style={{ width: HOUR_WIDTH }}
264
+ >
265
+ {hourLabel(hour)}
266
+ </button>
267
+ ))}
268
+ </div>
269
+
270
+ {rows.map((row) => {
271
+ const person = byAddress.get(row.address.toLowerCase());
272
+ const state = person && timeIsValid && covered ? availabilityDuring(person, eventStart, eventEnd) : undefined;
273
+ const hidden = person && person.status !== "available" ? person : undefined;
274
+ return (
275
+ <div key={row.address.toLowerCase()} className="flex border-b border-border last:border-b-0" style={{ height: ROW_HEIGHT }}>
276
+ <div className="sticky left-0 z-10 shrink-0 flex items-center gap-2 px-2 bg-surface border-r border-border" style={{ width: NAME_WIDTH }}>
277
+ <span aria-hidden="true" className="w-6 h-6 shrink-0 rounded-full bg-primary/15 text-primary-dark text-[11px] font-semibold flex items-center justify-center uppercase">
278
+ {row.name.charAt(0)}
279
+ </span>
280
+ <div className="min-w-0">
281
+ <div className="text-xs font-medium truncate">{row.name}</div>
282
+ <div className="text-[10px] text-text-muted truncate">{stateText(state, person?.status)}</div>
283
+ </div>
284
+ </div>
285
+ <div
286
+ role="img"
287
+ aria-label={`${row.name}: ${hidden ? stateText(undefined, hidden.status) : person ? busyDescription(person) : "Checking availability"}`}
288
+ onClick={handleTimelineClick}
289
+ className={["relative shrink-0 cursor-pointer", !person && lookUp.status === "loading" ? "animate-pulse bg-surface-alt" : ""].join(" ")}
290
+ style={{ width: TIMELINE_WIDTH, backgroundImage: GRID_LINES }}
291
+ >
292
+ <div aria-hidden="true" className="absolute inset-y-0 left-0 bg-surface-alt/70" style={{ width: WORK_START_HOUR * HOUR_WIDTH }} />
293
+ <div aria-hidden="true" className="absolute inset-y-0 right-0 bg-surface-alt/70" style={{ width: (24 - WORK_END_HOUR) * HOUR_WIDTH }} />
294
+ {hidden && (
295
+ <>
296
+ <div aria-hidden="true" className="absolute inset-0 opacity-30" style={{ backgroundImage: HATCH }} />
297
+ <span aria-hidden="true" className="sticky left-2 inline-block px-2 mt-3 text-[11px] text-text bg-surface rounded-sm">
298
+ {stateText(undefined, hidden.status)}
299
+ </span>
300
+ </>
301
+ )}
302
+ {person?.status === "available" &&
303
+ person.busy.map((block, index) => {
304
+ const from = minuteOfDay(block.startMs, dayKey, formZone, deviceZone);
305
+ const to = minuteOfDay(block.endMs, dayKey, formZone, deviceZone);
306
+ return to > from ? (
307
+ <div
308
+ key={index}
309
+ aria-hidden="true"
310
+ data-busy={block.tentative ? "tentative" : "busy"}
311
+ className={["absolute top-2 bottom-2 rounded-sm border border-danger", block.tentative ? "" : "bg-danger/60"].join(" ")}
312
+ style={{ left: (from / 60) * HOUR_WIDTH, width: ((to - from) / 60) * HOUR_WIDTH, backgroundImage: block.tentative ? TENTATIVE_HATCH : undefined }}
313
+ />
314
+ ) : null;
315
+ })}
316
+ </div>
317
+ </div>
318
+ );
319
+ })}
320
+
321
+ {showProposed && (
322
+ <div
323
+ aria-hidden="true"
324
+ data-proposed-time=""
325
+ className="absolute border-x-2 border-primary bg-primary/10 pointer-events-none z-[5]"
326
+ style={{
327
+ top: HEADER_HEIGHT,
328
+ bottom: 0,
329
+ left: NAME_WIDTH + (proposedFrom / 60) * HOUR_WIDTH,
330
+ width: ((proposedTo - proposedFrom) / 60) * HOUR_WIDTH,
331
+ }}
332
+ />
333
+ )}
334
+ </div>
335
+ </div>
336
+
337
+ <div role="status" aria-live="polite" className="text-sm">
338
+ {lookUp.status === "loading" && "Checking availability…"}
339
+ {lookUp.status === "ready" && !timeIsValid && "Set a start and an end time to see who is free."}
340
+ {lookUp.status === "ready" && timeIsValid && !covered && "Pick a time within this week to see who is free."}
341
+ {summary && <span className={summary.conflicts > 0 ? "font-medium text-danger" : "font-medium"}>{summaryLine(summary)}</span>}
342
+ </div>
343
+
344
+ {answered && timeIsValid && (
345
+ <section aria-labelledby="find-time-suggestions" className="flex flex-col gap-1.5">
346
+ <h4 id="find-time-suggestions" className="text-xs font-bold uppercase tracking-wide text-text-muted">
347
+ Suggested times
348
+ </h4>
349
+ {suggestions.length > 0 ? (
350
+ <ul className="flex flex-col gap-1">
351
+ {suggestions.map((ms) => {
352
+ const startWall = msToWallString(ms, formZone, deviceZone);
353
+ const endWall = msToWallString(ms + (eventEnd - eventStart), formZone, deviceZone);
354
+ return (
355
+ <li key={ms}>
356
+ <button type="button" onClick={() => c.setStart(startWall)} className="text-sm text-primary-dark hover:underline text-left">
357
+ {formatFormWhen(startWall, endWall, false)}
358
+ </button>
359
+ </li>
360
+ );
361
+ })}
362
+ </ul>
363
+ ) : (
364
+ <p className="text-sm text-text-muted">
365
+ {seen === 0
366
+ ? "There is no calendar to check, so no time can be suggested."
367
+ : `No time this week has everyone free between ${hourLabel(WORK_START_HOUR)} and ${hourLabel(WORK_END_HOUR)}.`}
368
+ </p>
369
+ )}
370
+ {unseen > 0 && (
371
+ <p className="text-xs text-text-muted">
372
+ Suggestions leave out {unseen} {unseen === 1 ? "person whose calendar" : "people whose calendars"} can&rsquo;t be seen.
373
+ </p>
374
+ )}
375
+ </section>
376
+ )}
377
+ </div>
378
+ );
379
+ }
@@ -0,0 +1,100 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React from "react";
6
+ import type { Attendee } from "@rapidmx/react-shared/calendar/calendarApi.js";
7
+ import type { ContactSuggestionOptions, RecipientSuggestion } from "@rapidmx/react-shared/mail/directoryApi.js";
8
+ import RecipientInput from "../mail/compose/RecipientInput.js";
9
+ import { applyGuestChips, guestChip } from "./eventFormat.js";
10
+
11
+ /** Above the event dialog's own backdrop (`z-[1000]`), which the suggestions would otherwise sit under. */
12
+ const DIALOG_SUGGESTIONS_Z_INDEX = 1100;
13
+
14
+ const VARIANT_CLASS = {
15
+ /** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
16
+ quiet: "w-full text-sm py-1 border-b border-transparent hover:border-border focus-within:border-primary",
17
+ /** A bordered box, as the full form's fields are. */
18
+ box: "w-full text-sm py-1.5 px-3 border border-border rounded-md bg-surface text-text focus-within:border-primary",
19
+ };
20
+
21
+ /** What a guests field holds: the guests added, what was committed but is not an address (kept in the field, flagged, until it is fixed or removed),
22
+ * and what is still being typed. */
23
+ export interface GuestEntries {
24
+ guests: Attendee[];
25
+ invalid: string[];
26
+ draft: string;
27
+ }
28
+
29
+ export interface GuestInputProps extends GuestEntries {
30
+ /** The input's id, and the prefix of its suggestions' ids. */
31
+ id: string;
32
+ /** The input's accessible name. */
33
+ label: string;
34
+ placeholder?: string;
35
+ variant: keyof typeof VARIANT_CLASS;
36
+ /** The guests are listed elsewhere (rows with a role and an answer): the field draws chips only for what is not an address. */
37
+ listedElsewhere?: boolean;
38
+ /** Addresses that are never added - the organizer's, or the people already invited. */
39
+ skipAddresses?: string[];
40
+ /** The mailbox the event is on, whose contacts are suggested too. */
41
+ mailboxUid?: string;
42
+ onChange: (next: GuestEntries) => void;
43
+ /** Loads suggestions; defaults to the lookup compose's recipient fields use. */
44
+ fetchSuggestions?: (query: string, options: ContactSuggestionOptions) => Promise<RecipientSuggestion[]>;
45
+ debounceMs?: number;
46
+ }
47
+
48
+ /** What `RecipientInput` calls `onChange` with; the entries come through `onEdit`. */
49
+ const IGNORED = () => undefined;
50
+
51
+ /**
52
+ * The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
53
+ * caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
54
+ * Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
55
+ * a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
56
+ * (as compose sends to one), which the mail server delivers to its members.
57
+ *
58
+ * The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
59
+ */
60
+ export default function GuestInput({
61
+ id,
62
+ label,
63
+ placeholder,
64
+ variant,
65
+ listedElsewhere,
66
+ skipAddresses,
67
+ mailboxUid,
68
+ guests,
69
+ invalid,
70
+ draft,
71
+ onChange,
72
+ fetchSuggestions,
73
+ debounceMs,
74
+ }: GuestInputProps) {
75
+ const value = [...guests.map(guestChip), ...invalid, draft].filter((part) => part.length > 0).join(", ");
76
+ const listed = new Set(listedElsewhere ? guests.map(guestChip) : []);
77
+
78
+ return (
79
+ <RecipientInput
80
+ id={id}
81
+ label={label}
82
+ ariaLabel={label}
83
+ listLabel="Guests"
84
+ placeholder={placeholder}
85
+ className={VARIANT_CLASS[variant]}
86
+ dropdownZIndex={DIALOG_SUGGESTIONS_Z_INDEX}
87
+ value={value}
88
+ initialPending={draft}
89
+ mailboxUid={mailboxUid}
90
+ hideChip={(chip) => listed.has(chip)}
91
+ fetchSuggestions={fetchSuggestions}
92
+ debounceMs={debounceMs}
93
+ onChange={IGNORED}
94
+ onEdit={(chips, pending) => {
95
+ const merged = applyGuestChips(guests, chips, skipAddresses);
96
+ onChange({ guests: merged.attendees, invalid: merged.invalid, draft: pending });
97
+ }}
98
+ />
99
+ );
100
+ }
@@ -0,0 +1,70 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { ComponentType, useEffect, useState } from "react";
6
+ import type { DescriptionEditorProps } from "./DescriptionEditor.js";
7
+
8
+ let loading: Promise<ComponentType<DescriptionEditorProps>> | undefined;
9
+ /** The editor once its chunk has arrived, so an editor drawn again (the quick popover growing into the card) starts as the editor, not as the placeholder. */
10
+ let loaded: ComponentType<DescriptionEditorProps> | undefined;
11
+
12
+ /** Fetches the editor's chunk once. A failed download is forgotten, so asking again tries again. */
13
+ function loadDescriptionEditor(): Promise<ComponentType<DescriptionEditorProps>> {
14
+ loading ??= import("./DescriptionEditor.js").then(
15
+ (module) => (loaded = module.default),
16
+ (err: unknown) => {
17
+ loading = undefined;
18
+ throw err;
19
+ },
20
+ );
21
+ return loading;
22
+ }
23
+
24
+ /**
25
+ * `DescriptionEditor`, loaded when it is first drawn. The editor is TipTap and ProseMirror - a good deal of code that the calendar, and the reading pane's
26
+ * invitation card, only need once somebody opens a description - so it is fetched then rather than with the page. Not `React.lazy()`: a chunk that fails to
27
+ * download (offline, or a deploy that replaced it) is said so here with a retry, instead of throwing through the tree above.
28
+ */
29
+ export default function LazyDescriptionEditor(props: DescriptionEditorProps) {
30
+ const [Editor, setEditor] = useState<ComponentType<DescriptionEditorProps> | null>(() => loaded ?? null);
31
+ const [failed, setFailed] = useState(false);
32
+ const [attempt, setAttempt] = useState(0);
33
+
34
+ useEffect(() => {
35
+ let current = true;
36
+ loadDescriptionEditor().then(
37
+ (component) => current && setEditor(() => component),
38
+ () => current && setFailed(true),
39
+ );
40
+ return () => {
41
+ current = false;
42
+ };
43
+ }, [attempt]);
44
+
45
+ if (Editor) {
46
+ return <Editor {...props} />;
47
+ }
48
+ if (failed) {
49
+ return (
50
+ <p role="alert" className="text-sm text-danger">
51
+ The description editor couldn&rsquo;t be loaded.{" "}
52
+ <button
53
+ type="button"
54
+ className="underline"
55
+ onClick={() => {
56
+ setFailed(false);
57
+ setAttempt((count) => count + 1);
58
+ }}
59
+ >
60
+ Try again
61
+ </button>
62
+ </p>
63
+ );
64
+ }
65
+ return (
66
+ <div aria-busy="true" className="min-h-[7rem] rounded-md border border-border bg-surface-alt text-sm text-text-muted px-3 py-2">
67
+ Loading the editor&hellip;
68
+ </div>
69
+ );
70
+ }