@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,71 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { HiOutlineArchiveBox, HiOutlineFolder } from "react-icons/hi2";
8
+ import { useSwipe } from "../../gestures/useSwipe.js";
9
+ /** How long the row takes to leave, or come back - `motion-reduce` skips it. */
10
+ const SLIDE = "transform 150ms ease-out";
11
+ /**
12
+ * A row of a list of mail that slides with a finger: right to left it takes the row away to Archive (a green panel with the
13
+ * archive icon follows the row in), left to right it comes back and asks for a folder to move it to (a blue panel with a
14
+ * folder). The panels are children of the row placed just outside its edges, so they travel with it and fill the gap it leaves
15
+ * without the row's own markup, or the list's, changing; the list has to clip them (`overflow-x-clip`).
16
+ *
17
+ * Only a swipe that starts out horizontal counts (see `useSwipe()`), so scrolling the list is untouched, and it is an addition
18
+ * to, never the only way of, archiving and moving: the selection bar and the reading pane do the same.
19
+ */
20
+ export default function SwipeRow({ as: Tag = "div", enabled, onArchive, onMove, children, className, style, ...rest }) {
21
+ const [offset, setOffset] = useState(0);
22
+ const [width, setWidth] = useState(0);
23
+ /** The row is on its way out (or waiting for the server to confirm it), so a finger can't drag it. */
24
+ const [leaving, setLeaving] = useState(false);
25
+ const [dragging, setDragging] = useState(false);
26
+ const element = useRef(null);
27
+ const mounted = useRef(true);
28
+ useEffect(() => {
29
+ mounted.current = true;
30
+ return () => {
31
+ mounted.current = false;
32
+ };
33
+ }, []);
34
+ const swipe = useSwipe({
35
+ enabled: enabled && !leaving,
36
+ onDrag: (next) => {
37
+ setDragging(next !== 0);
38
+ setOffset(next);
39
+ },
40
+ onSwipe: (direction) => {
41
+ if (direction === "right") {
42
+ onMove();
43
+ return;
44
+ }
45
+ // A swipe is only ever heard by the row's own element, which is what `element` holds.
46
+ const rowWidth = element.current.clientWidth;
47
+ setWidth(rowWidth);
48
+ setLeaving(true);
49
+ setOffset(-rowWidth);
50
+ void onArchive().then((archived) => {
51
+ // Unmounted on success, once the list has dropped the row; a refusal brings it back.
52
+ if (!archived && mounted.current) {
53
+ setLeaving(false);
54
+ setOffset(0);
55
+ }
56
+ });
57
+ },
58
+ });
59
+ if (!enabled) {
60
+ return (_jsx(Tag, { className: className, style: style, ...rest, children: children }));
61
+ }
62
+ const active = offset !== 0;
63
+ return (_jsxs(Tag, { ...rest, ...swipe.handlers, ref: (node) => {
64
+ element.current = node;
65
+ }, className: className, style: {
66
+ ...style,
67
+ ...swipe.style,
68
+ transform: active ? `translateX(${offset}px)` : undefined,
69
+ transition: dragging ? "none" : SLIDE,
70
+ }, "data-swiping": active ? "true" : undefined, children: [children, (active || leaving) && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", "data-swipe-panel": "move", className: "absolute inset-y-0 right-full w-full bg-primary text-white flex items-center justify-end pr-6", children: _jsx(HiOutlineFolder, { size: 24 }) }), _jsx("span", { "aria-hidden": "true", "data-swipe-panel": "archive", style: width ? { width } : undefined, className: "absolute inset-y-0 left-full w-full bg-success text-white flex items-center justify-start pl-6", children: _jsx(HiOutlineArchiveBox, { size: 24 }) })] }))] }));
71
+ }
@@ -10,6 +10,7 @@ import { assembleDraft, attachmentContentUrl, createDraft, deleteMessage, getMai
10
10
  import { listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
11
11
  import { buildComposeBodyHtml } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
12
12
  import { peekMailboxWritability, useMailboxWritability } from "../writableMailboxes.js";
13
+ import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
13
14
  import { resolveRecipientEncryption } from "@rapidmx/react-shared/crypto/composeSecurity.js";
14
15
  import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
15
16
  import { findActivePublicKey, getEncryptionPolicy, lookupKeys } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
@@ -219,11 +220,13 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
219
220
  if (cancelled) {
220
221
  return;
221
222
  }
222
- setMailboxes(result);
223
+ // Listed with the caller's own (primary) mailbox first, and defaulted to below.
224
+ const ordered = orderMailboxes(result, userUid);
225
+ setMailboxes(ordered);
223
226
  // Never default to a mailbox already known to be view-only (see the effect below for one
224
227
  // that turns out to be).
225
- const candidates = result.filter((mb) => peekMailboxWritability(mb, userUid, trusted) !== false);
226
- setFromMailboxUid((current) => current ?? (candidates.find((mb) => mb.ownerUserUid === userUid) ?? candidates[0])?.uid);
228
+ const candidates = ordered.filter((mb) => peekMailboxWritability(mb, userUid, trusted) !== false);
229
+ setFromMailboxUid((current) => current ?? primaryMailboxUid(candidates, userUid));
227
230
  })
228
231
  .catch((err) => {
229
232
  // Only fatal when there's no mailbox to fall back on - a session that already names one just
@@ -481,7 +484,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
481
484
  if (!fromIsViewOnly || hasUploads) {
482
485
  return;
483
486
  }
484
- const ordered = [...mailboxes.filter((mb) => mb.ownerUserUid === userUid), ...mailboxes.filter((mb) => mb.ownerUserUid !== userUid)];
487
+ const ordered = orderMailboxes(mailboxes, userUid);
485
488
  const alternative = ordered.find((mb) => writability[mb.uid] === true) ?? ordered.find((mb) => writability[mb.uid] === undefined);
486
489
  if (alternative) {
487
490
  handleFromChange(alternative.uid);
@@ -685,6 +688,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
685
688
  encryptRequested: current.encryptRequested,
686
689
  forcePlaintext,
687
690
  saved: saveInFlightRef.current,
691
+ threading: session.threading,
688
692
  };
689
693
  if (request.to.length === 0) {
690
694
  setSendError("At least one recipient is required.");
@@ -1141,11 +1145,16 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
1141
1145
  return (_jsxs(_Fragment, { children: [_jsx("div", { role: "dialog", "aria-label": title, className: "w-64 shrink-0 bg-surface border border-border border-b-0 rounded-t-md shadow-modal", children: _jsxs("div", { className: "h-10 flex items-center justify-between gap-2 px-3 rounded-t-md bg-primary-darker text-white cursor-pointer", onClick: onToggleMinimize, children: [_jsx("span", { className: "text-sm font-medium truncate", children: title }), _jsxs("div", { className: "flex items-center gap-0.5", onClick: (e) => e.stopPropagation(), children: [_jsx(HeaderButton, { label: "Restore", onClick: onToggleMinimize, icon: HiOutlineArrowsPointingOut }), _jsx(HeaderButton, { label: "Discard draft", onClick: handleDiscard, icon: HiOutlineXMark, disabled: closing })] })] }) }), discardModal] }));
1142
1146
  }
1143
1147
  return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: windowRef, role: "dialog", "data-shortcut-scope": "compose", "aria-labelledby": titleId, style: !isMobile && manualSize ? { width: manualSize.width, height: manualSize.height } : undefined, className: [
1144
- "relative shrink-0 flex flex-col bg-surface border border-border shadow-modal overflow-hidden",
1148
+ "shrink-0 flex flex-col bg-surface border border-border shadow-modal overflow-hidden",
1149
+ // `fixed` and `relative` may not both be on the element: Tailwind emits `.relative` after `.fixed`, so it wins and the
1150
+ // sheet sat in the flow of the little fixed container the windows are stacked in instead of covering the screen.
1145
1151
  isMobile
1146
1152
  ? "fixed inset-0 w-full h-full rounded-none border-0"
1147
- : ["border-b-0 rounded-t-md", manualSize ? "" : expanded ? "w-[720px] h-[85vh]" : "w-[480px] h-[520px]"].join(" "),
1148
- ].join(" "), children: [!isMobile && (_jsxs(_Fragment, { children: [_jsx("div", { onPointerDown: handleResizeStart("top"), "aria-hidden": "true", className: "absolute top-0 left-0 right-0 h-1.5 cursor-ns-resize z-20" }), _jsx("div", { onPointerDown: handleResizeStart("left"), "aria-hidden": "true", className: "absolute top-0 left-0 bottom-0 w-1.5 cursor-ew-resize z-20" }), _jsx("div", { onPointerDown: handleResizeStart("corner"), role: "separator", "aria-label": "Resize", className: "absolute top-0 left-0 w-3 h-3 cursor-nwse-resize z-30" })] })), _jsxs("div", { className: "h-10 shrink-0 flex items-center justify-between gap-2 px-3 bg-primary-darker text-white cursor-pointer", onClick: onToggleMinimize, children: [_jsx("span", { id: titleId, className: "text-sm font-medium truncate", children: title }), _jsxs("div", { className: "flex items-center gap-0.5", onClick: (e) => e.stopPropagation(), children: [_jsx(HeaderButton, { label: "Minimize", onClick: onToggleMinimize, icon: HiOutlineMinus }), !isMobile && (_jsx(HeaderButton, { label: expanded ? "Collapse" : "Expand", onClick: toggleExpanded, icon: expanded ? HiOutlineArrowsPointingIn : HiOutlineArrowsPointingOut })), _jsx(HeaderButton, { label: "Close", onClick: () => void handleClose(), icon: HiOutlineXMark, disabled: closing, shortcut: SHORTCUTS.compose.close })] })] }), _jsxs("div", { className: "flex-1 min-h-0 flex flex-col", children: [(folderError || draftError || sendError || attachError || discardError) && (_jsxs("div", { className: "px-3 pt-2 shrink-0 max-h-[45%] overflow-y-auto", children: [folderError && _jsx(Alert, { children: folderError }), draftError && _jsx(Alert, { children: draftError }), sendError && _jsx(Alert, { children: sendError }), attachError && _jsx(Alert, { children: attachError }), discardError && _jsx(Alert, { children: discardError })] })), securityBlock && (_jsx("div", { className: "px-3 pt-2", children: _jsxs(Alert, { children: [_jsx("p", { className: "mb-2", children: securityBlock.message }), _jsx(Button, { type: "button", onClick: () => void submit(true, blockedScheduleIso), children: securityBlock.overrideLabel })] }) })), fromOptions.length > 1 && (_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-from-${id}`, className: "text-xs text-text-muted shrink-0", children: "From" }), _jsx("select", { id: `compose-from-${id}`, className: `${FIELD_INPUT} disabled:opacity-55`, value: mailboxUid ?? "", disabled: hasUploads, title: hasUploads ? "The sender can't be changed after adding attachments or images." : undefined, onChange: (e) => handleFromChange(e.target.value), children: fromOptions.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName ? `${mb.displayName} <${mb.primarySmtpAddress}>` : mb.primarySmtpAddress, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-to-${id}`, className: "text-xs text-text-muted shrink-0", children: "To" }), _jsx(RecipientInput, { id: `compose-to-${id}`, label: "To", value: to, mailboxUid: mailboxUid, onChange: setTo, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)), autoFocus: focusToOnMount }), !showCcBcc && (_jsx("button", { type: "button", onClick: () => setShowCcBcc(true), className: "text-xs text-text-muted hover:text-text shrink-0", children: "Cc Bcc" }))] }), showCcBcc && (_jsxs(_Fragment, { children: [_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-cc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Cc" }), _jsx(RecipientInput, { id: `compose-cc-${id}`, label: "Cc", value: cc, mailboxUid: mailboxUid, onChange: setCc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] }), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-bcc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Bcc" }), _jsx(RecipientInput, { id: `compose-bcc-${id}`, label: "Bcc", value: bcc, mailboxUid: mailboxUid, onChange: setBcc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] })] })), (() => {
1153
+ : ["relative border-b-0 rounded-t-md", manualSize ? "" : expanded ? "w-[720px] h-[85vh]" : "w-[480px] h-[520px]"].join(" "),
1154
+ ].join(" "), children: [!isMobile && (_jsxs(_Fragment, { children: [_jsx("div", { onPointerDown: handleResizeStart("top"), "aria-hidden": "true", className: "absolute top-0 left-0 right-0 h-1.5 cursor-ns-resize z-20" }), _jsx("div", { onPointerDown: handleResizeStart("left"), "aria-hidden": "true", className: "absolute top-0 left-0 bottom-0 w-1.5 cursor-ew-resize z-20" }), _jsx("div", { onPointerDown: handleResizeStart("corner"), role: "separator", "aria-label": "Resize", className: "absolute top-0 left-0 w-3 h-3 cursor-nwse-resize z-30" })] })), _jsxs("div", { className: "h-10 shrink-0 flex items-center justify-between gap-2 px-3 bg-primary-darker text-white cursor-pointer", onClick: onToggleMinimize, children: [_jsx("span", { id: titleId, className: "text-sm font-medium truncate", children: title }), _jsxs("div", { className: "flex items-center gap-0.5", onClick: (e) => e.stopPropagation(), children: [_jsx(HeaderButton, { label: "Minimize", onClick: onToggleMinimize, icon: HiOutlineMinus }), !isMobile && (_jsx(HeaderButton, { label: expanded ? "Collapse" : "Expand", onClick: toggleExpanded, icon: expanded ? HiOutlineArrowsPointingIn : HiOutlineArrowsPointingOut })), _jsx(HeaderButton, { label: "Close", onClick: () => void handleClose(), icon: HiOutlineXMark, disabled: closing, shortcut: SHORTCUTS.compose.close })] })] }), _jsxs("div", { className: "flex-1 min-h-0 flex flex-col", children: [(folderError || draftError || sendError || attachError || discardError) && (_jsxs("div", { className: "px-3 pt-2 shrink-0 max-h-[45%] overflow-y-auto", children: [folderError && _jsx(Alert, { children: folderError }), draftError && _jsx(Alert, { children: draftError }), sendError && _jsx(Alert, { children: sendError }), attachError && _jsx(Alert, { children: attachError }), discardError && _jsx(Alert, { children: discardError })] })), securityBlock && (_jsx("div", { className: "px-3 pt-2", children: _jsxs(Alert, { children: [_jsx("p", { className: "mb-2", children: securityBlock.message }), _jsx(Button, { type: "button", onClick: () => void submit(true, blockedScheduleIso), children: securityBlock.overrideLabel })] }) })), fromOptions.length > 1 && (_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-from-${id}`, className: "text-xs text-text-muted shrink-0", children: "From" }), _jsx("select", { id: `compose-from-${id}`, className: `${FIELD_INPUT} disabled:opacity-55`,
1155
+ // Two or more options only exist once the mailboxes have loaded, and the same answer that lets them list
1156
+ // (one that isn't known to be view-only) is what picks the default sender, so a sender is always chosen by now.
1157
+ value: mailboxUid, disabled: hasUploads, title: hasUploads ? "The sender can't be changed after adding attachments or images." : undefined, onChange: (e) => handleFromChange(e.target.value), children: fromOptions.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName ? `${mb.displayName} <${mb.primarySmtpAddress}>` : mb.primarySmtpAddress, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-to-${id}`, className: "text-xs text-text-muted shrink-0", children: "To" }), _jsx(RecipientInput, { id: `compose-to-${id}`, label: "To", value: to, mailboxUid: mailboxUid, onChange: setTo, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)), autoFocus: focusToOnMount }), !showCcBcc && (_jsx("button", { type: "button", onClick: () => setShowCcBcc(true), className: "text-xs text-text-muted hover:text-text shrink-0", children: "Cc Bcc" }))] }), showCcBcc && (_jsxs(_Fragment, { children: [_jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-cc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Cc" }), _jsx(RecipientInput, { id: `compose-cc-${id}`, label: "Cc", value: cc, mailboxUid: mailboxUid, onChange: setCc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] }), _jsxs("div", { className: FIELD_ROW, children: [_jsx("label", { htmlFor: `compose-bcc-${id}`, className: "text-xs text-text-muted shrink-0", children: "Bcc" }), _jsx(RecipientInput, { id: `compose-bcc-${id}`, label: "Bcc", value: bcc, mailboxUid: mailboxUid, onChange: setBcc, onBlur: (value) => checkRecipientDiscovery(parseAddresses(value)), onCommit: (value) => checkRecipientDiscovery(parseAddresses(value)) })] })] })), (() => {
1149
1158
  // De-duplicated by the same normalized key `recipientStatuses` itself is keyed by - two
1150
1159
  // fields (or one field twice) naming the same mailbox in different casing now resolve to
1151
1160
  // one shared status object (see `addressCacheKey()`'s own doc comment), so without this
@@ -22,6 +22,26 @@ export interface RecipientInputProps {
22
22
  disabled?: boolean;
23
23
  /** Focuses the input when it mounts - a new message starts in To. */
24
24
  autoFocus?: boolean;
25
+ /** Called whenever the field's entries change: every committed entry (chips - a picked suggestion, a finished or
26
+ * pasted one, or a typed one committed by Enter or leaving the field) and the text still being typed (`""` after a
27
+ * commit). Lets a field that keeps its entries as something other than text (attendees) read them without parsing
28
+ * `value` and guessing which part of it is still being typed. */
29
+ onEdit?: (chips: string[], pending: string) => void;
30
+ /** Text being typed already when the input mounts (the last recipient of `value`), so a field that is remounted
31
+ * (a popover growing into a form) keeps it in the input instead of turning it into a chip. */
32
+ initialPending?: string;
33
+ /** Chips this returns true for are not drawn (they stay in the value, and out of the suggestions): a field that
34
+ * lists some entries itself. Backspace in an empty input removes the last chip that is drawn. */
35
+ hideChip?: (chip: string) => boolean;
36
+ placeholder?: string;
37
+ /** The input's accessible name, for a field without a `<label htmlFor>`. */
38
+ ariaLabel?: string;
39
+ /** The chip list's accessible name; `${label} recipients` by default. */
40
+ listLabel?: string;
41
+ /** Classes for the field's outer element, after its own (a border, padding). */
42
+ className?: string;
43
+ /** The suggestions' `z-index`, for a field inside an overlay that stacks above the default's (`z-[60]`). */
44
+ dropdownZIndex?: number;
25
45
  }
26
46
  /**
27
47
  * A To/Cc/Bcc field: committed recipients as removable chips, and a WAI-ARIA combobox for the one being typed that
@@ -33,4 +53,4 @@ export interface RecipientInputProps {
33
53
  * least two characters, and a newer query aborts an older request. Arrow keys move through them, Enter or Tab picks
34
54
  * one (as `Name <address>`), Escape closes them, and Backspace in an empty input removes the last chip.
35
55
  */
36
- export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions, debounceMs, disabled, autoFocus, }: RecipientInputProps): React.JSX.Element;
56
+ export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions, debounceMs, disabled, autoFocus, onEdit, initialPending, hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }: RecipientInputProps): React.JSX.Element;
@@ -41,8 +41,8 @@ function dropdownPosition(anchor) {
41
41
  * least two characters, and a newer query aborts an older request. Arrow keys move through them, Enter or Tab picks
42
42
  * one (as `Name <address>`), Escape closes them, and Backspace in an empty input removes the last chip.
43
43
  */
44
- export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, }) {
45
- const [pending, setPending] = useState("");
44
+ export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, onEdit, initialPending = "", hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }) {
45
+ const [pending, setPending] = useState(initialPending);
46
46
  const [suggestions, setSuggestions] = useState([]);
47
47
  const [open, setOpen] = useState(false);
48
48
  const [activeIndex, setActiveIndex] = useState(0);
@@ -58,6 +58,8 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
58
58
  const chips = pendingActive ? tokens.slice(0, -1) : tokens;
59
59
  const inputValue = pendingActive ? pending : "";
60
60
  const query = pendingActive ? pendingText : "";
61
+ const drawn = chips.map((chip, index) => ({ chip, index })).filter(({ chip }) => !hideChip?.(chip));
62
+ const lastDrawn = drawn.length > 0 ? drawn[drawn.length - 1].index : -1;
61
63
  const listboxId = `${id}-suggestions`;
62
64
  const optionId = (index) => `${id}-suggestion-${index}`;
63
65
  useEffect(() => {
@@ -113,6 +115,7 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
113
115
  });
114
116
  function emit(nextChips, nextPending) {
115
117
  onChange([...nextChips, nextPending.trim()].filter((part) => part.length > 0).join(", "));
118
+ onEdit?.(nextChips, nextPending.trim());
116
119
  }
117
120
  function handleInputChange(e) {
118
121
  const { finished, rest } = splitTypedRecipients(e.target.value);
@@ -122,17 +125,22 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
122
125
  emit([...chips, ...finished], nextPending);
123
126
  }
124
127
  function commitPending() {
125
- const next = [...chips, pendingText].filter((part) => part.length > 0).join(", ");
128
+ const committed = [...chips, pendingText].filter((part) => part.length > 0);
126
129
  setPending("");
127
130
  setOpen(false);
128
- return next;
131
+ if (pendingText) {
132
+ onEdit?.(committed, "");
133
+ }
134
+ return committed.join(", ");
129
135
  }
130
136
  function select(suggestion) {
131
- const next = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })].join(", ");
137
+ const committed = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })];
138
+ const next = committed.join(", ");
132
139
  setPending("");
133
140
  setOpen(false);
134
141
  setSuggestions([]);
135
142
  onChange(next);
143
+ onEdit?.(committed, "");
136
144
  onCommit?.(next);
137
145
  }
138
146
  function removeChip(index) {
@@ -181,9 +189,9 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
181
189
  }
182
190
  return;
183
191
  case "Backspace":
184
- if (inputValue === "" && chips.length > 0) {
192
+ if (inputValue === "" && lastDrawn >= 0) {
185
193
  e.preventDefault();
186
- emit(chips.slice(0, -1), "");
194
+ emit(chips.filter((_, i) => i !== lastDrawn), "");
187
195
  }
188
196
  return;
189
197
  }
@@ -192,12 +200,12 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
192
200
  const next = commitPending();
193
201
  onBlur?.(next);
194
202
  }
195
- return (_jsxs("div", { ref: anchorRef, className: "flex-1 min-w-0 flex flex-wrap items-center gap-1", children: [chips.length > 0 && (_jsx("ul", { role: "list", "aria-label": `${label} recipients`, className: "contents", children: chips.map((chip, index) => {
203
+ return (_jsxs("div", { ref: anchorRef, className: `flex-1 min-w-0 flex flex-wrap items-center gap-1${className ? ` ${className}` : ""}`, children: [drawn.length > 0 && (_jsx("ul", { role: "list", "aria-label": listLabel ?? `${label} recipients`, className: "contents", children: drawn.map(({ chip, index }) => {
196
204
  const recipient = parseRecipient(chip);
197
205
  const valid = isValidRecipientAddress(recipient.address);
198
206
  const name = recipient.displayName || recipient.address;
199
207
  return (_jsxs("li", { title: chip, className: `flex items-center gap-1 max-w-full min-w-0 rounded-pill border pl-2 pr-0.5 py-px text-xs ${valid ? "bg-surface-alt border-border text-text" : "bg-danger-bg border-danger text-danger"}`, children: [_jsx("span", { className: "truncate", children: name }), !valid && _jsx("span", { className: "sr-only", children: " (not a valid email address)" }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Remove ${recipient.address}`, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: () => removeChip(index), className: "shrink-0 w-4 h-4 flex items-center justify-center rounded-pill text-text-muted hover:bg-border hover:text-text", children: _jsx(HiOutlineXMark, { size: 10 }) })] }, `${index}-${chip}`));
200
- }) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
208
+ }) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-label": ariaLabel, placeholder: placeholder, "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
201
209
  position &&
202
- createPortal(_jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${label} suggestions`, onMouseDown: (e) => e.preventDefault(), style: { position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight }, className: "z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1", children: visible.map((suggestion, index) => (_jsxs("li", { id: optionId(index), role: "option", "aria-selected": index === active, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActiveIndex(index), onClick: () => select(suggestion), className: `flex items-center gap-2 px-3 py-1.5 cursor-pointer ${index === active ? "bg-primary/10" : ""}`, children: [_jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate text-sm text-text", children: suggestion.displayName || suggestion.address }), suggestion.displayName && _jsx("span", { className: "block truncate text-xs text-text-muted", children: suggestion.address })] }), _jsx("span", { className: "shrink-0 text-[11px] leading-4 px-1.5 rounded-pill bg-surface-alt text-text-muted", children: KIND_LABELS[suggestion.kind] ?? "Directory" })] }, `${index}-${suggestion.address}`))) }), document.body)] }));
210
+ createPortal(_jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${label} suggestions`, onMouseDown: (e) => e.preventDefault(), style: { position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight, zIndex: dropdownZIndex }, className: "z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1", children: visible.map((suggestion, index) => (_jsxs("li", { id: optionId(index), role: "option", "aria-selected": index === active, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActiveIndex(index), onClick: () => select(suggestion), className: `flex items-center gap-2 px-3 py-1.5 cursor-pointer ${index === active ? "bg-primary/10" : ""}`, children: [_jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate text-sm text-text", children: suggestion.displayName || suggestion.address }), suggestion.displayName && _jsx("span", { className: "block truncate text-xs text-text-muted", children: suggestion.address })] }), _jsx("span", { className: "shrink-0 text-[11px] leading-4 px-1.5 rounded-pill bg-surface-alt text-text-muted", children: KIND_LABELS[suggestion.kind] ?? "Directory" })] }, `${index}-${suggestion.address}`))) }), document.body)] }));
203
211
  }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ export interface InviteRowChipProps {
4
+ message: Message;
5
+ /** Called with the row's message carrying the reader's new answer (`meetingResponse`), after an RSVP, so the list can show it. */
6
+ onResponded?: (updated: Message) => void;
7
+ /** The chip's horizontal margins - the row's own padding. Default `mx-4`. */
8
+ className?: string;
9
+ }
10
+ /** Whether a list row draws the chip: a meeting request the server can read (an encrypted message's calendar file is inside its ciphertext). */
11
+ export declare function showsInviteChip(message: Pick<Message, "meetingMethod" | "encrypted">): boolean;
12
+ /**
13
+ * The line a meeting request gets in a list of mail, under its preview, as Outlook draws it: a calendar icon, when the meeting starts (in the reader's
14
+ * zone), under that whether it conflicts with the reader's calendar - or, once answered, the answer - and an RSVP button that opens the day view with
15
+ * Accept, Decline and Tentative (`InviteRsvpPopover`).
16
+ *
17
+ * The invitation is looked up (once, through the shared cache) only for a row that `showsInviteChip()` - the rest of the list is never asked about. Until
18
+ * it arrives, or when the lookup fails, the chip says only "Meeting request", with no button.
19
+ *
20
+ * It lives beside the row's open button, not inside it (a button can't hold a button), and keeps taps and touches to itself: pressing RSVP neither
21
+ * opens the row nor starts a swipe of it. That holds for the popover too, which React nests under the chip even though it is drawn on the page.
22
+ */
23
+ export default function InviteRowChip({ message, onResponded, className }: InviteRowChipProps): React.JSX.Element | null;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useRef, useState } from "react";
7
+ import { HiOutlineCalendarDays } from "react-icons/hi2";
8
+ import { ANSWER_LABEL, formatRowStart } from "./inviteFormat.js";
9
+ import { useMessageInvite } from "./inviteStore.js";
10
+ import InviteRsvpPopover from "./InviteRsvpPopover.js";
11
+ /** Whether a list row draws the chip: a meeting request the server can read (an encrypted message's calendar file is inside its ciphertext). */
12
+ export function showsInviteChip(message) {
13
+ return message.meetingMethod === "REQUEST" && !message.encrypted;
14
+ }
15
+ /**
16
+ * The line a meeting request gets in a list of mail, under its preview, as Outlook draws it: a calendar icon, when the meeting starts (in the reader's
17
+ * zone), under that whether it conflicts with the reader's calendar - or, once answered, the answer - and an RSVP button that opens the day view with
18
+ * Accept, Decline and Tentative (`InviteRsvpPopover`).
19
+ *
20
+ * The invitation is looked up (once, through the shared cache) only for a row that `showsInviteChip()` - the rest of the list is never asked about. Until
21
+ * it arrives, or when the lookup fails, the chip says only "Meeting request", with no button.
22
+ *
23
+ * It lives beside the row's open button, not inside it (a button can't hold a button), and keeps taps and touches to itself: pressing RSVP neither
24
+ * opens the row nor starts a swipe of it. That holds for the popover too, which React nests under the chip even though it is drawn on the page.
25
+ */
26
+ export default function InviteRowChip({ message, onResponded, className = "mx-4" }) {
27
+ const eligible = showsInviteChip(message);
28
+ const { invite, setInvite } = useMessageInvite(message.uid, eligible);
29
+ const [open, setOpen] = useState(false);
30
+ const rsvpRef = useRef(null);
31
+ if (!eligible) {
32
+ return null;
33
+ }
34
+ const answer = invite?.response ?? message.meetingResponse;
35
+ const conflicts = invite?.conflicts.length ?? 0;
36
+ const start = invite ? formatRowStart(invite) : undefined;
37
+ function answered(updated) {
38
+ setInvite(updated);
39
+ onResponded?.({ ...message, meetingResponse: updated.response ?? message.meetingResponse });
40
+ }
41
+ const stop = (event) => event.stopPropagation();
42
+ return (_jsxs("div", { "data-invite-chip": true, onClick: stop, onTouchStart: stop, onTouchMove: stop, onTouchEnd: stop, onTouchCancel: stop, className: [className, "mb-2 -mt-1 flex items-center gap-2 rounded-sm border border-border bg-surface px-2 py-1.5 text-xs font-normal"].join(" "), children: [_jsx(HiOutlineCalendarDays, { size: 16, "aria-hidden": "true", className: "shrink-0 text-text-muted" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-text", children: start ?? "Meeting request" }), invite && (_jsx("div", { className: [
43
+ "truncate",
44
+ answer === "accepted" ? "text-success" : answer === "declined" ? "text-danger" : conflicts > 0 ? "text-danger" : "text-text-muted",
45
+ ].join(" "), children: answer ? ANSWER_LABEL[answer] : conflicts > 0 ? `Conflicts with ${conflicts} ${conflicts === 1 ? "event" : "events"}` : "No conflicts" }))] }), invite?.canRespond && !invite.isOrganizer && (_jsx("button", { ref: rsvpRef, type: "button", "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": `RSVP to ${invite.summary?.trim() || "this meeting"}`, onClick: () => setOpen((wasOpen) => !wasOpen), className: "shrink-0 py-1 px-3 rounded-sm border border-border text-xs font-semibold text-text hover:bg-surface-alt", children: "RSVP" })), open && invite && (_jsx(InviteRsvpPopover, { messageUid: message.uid, invite: invite, anchorRef: rsvpRef, onDone: answered, onClose: () => setOpen(false) }))] }));
46
+ }
@@ -0,0 +1,20 @@
1
+ import React, { RefObject } from "react";
2
+ import { MessageInvite } from "@rapidmx/react-shared/calendar/inviteApi.js";
3
+ export interface InviteRsvpPopoverProps {
4
+ messageUid: string;
5
+ invite: MessageInvite;
6
+ /** The RSVP button: the popover hangs from it, and focus goes back to it when the popover closes. */
7
+ anchorRef: RefObject<HTMLElement | null>;
8
+ /** Called with the invitation as it stands after an answer or a proposal. The popover has closed itself. */
9
+ onDone: (invite: MessageInvite) => void;
10
+ onClose: () => void;
11
+ }
12
+ /**
13
+ * What the RSVP button opens: an anchored popover on a desktop, a dialog on a phone. A day view of the reader's schedule around the meeting, with
14
+ * the invitation highlighted and its conflicts marked, and Accept, Decline, a "?" for Tentative and a "..." menu (Propose new time, Open in calendar).
15
+ * An answer is sent to the server, which puts the meeting on the calendar and replies to the organizer; the popover then closes and hands the
16
+ * updated invitation to its caller. A failure raises a pop-up and leaves the popover open.
17
+ *
18
+ * Focus moves to the first button when it opens, Escape closes it (a menu open inside it first), and focus returns to the RSVP button.
19
+ */
20
+ export default function InviteRsvpPopover({ messageUid, invite, anchorRef, onDone, onClose }: InviteRsvpPopoverProps): React.JSX.Element;
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useId, useRef, useState } from "react";
7
+ import { HiOutlineCheck, HiOutlineEllipsisHorizontal } from "react-icons/hi2";
8
+ import { proposeNewTime, respondToMessageInvite } from "@rapidmx/react-shared/calendar/inviteApi.js";
9
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
11
+ import PopoverPortal from "@rapidmx/react-shared/components/overlays/PopoverPortal.js";
12
+ import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
13
+ import { notifyApiError } from "../../../notifications/apiErrors.js";
14
+ import { calendarHref, conflictSummary, formatInviteWhen } from "./inviteFormat.js";
15
+ import InviteTimeline from "./InviteTimeline.js";
16
+ import ProposeTimeForm from "./ProposeTimeForm.js";
17
+ /** The popover's width, and the room it leaves the window's edges. */
18
+ const WIDTH = 360;
19
+ const WINDOW_MARGIN = 16;
20
+ /** What the popover is tall for its own text and footer, before the day view (px), and how tall the day view is per hour drawn. */
21
+ const CHROME_HEIGHT = 190;
22
+ const CONFLICT_LINE_HEIGHT = 22;
23
+ const HOURS_HEIGHT = 36;
24
+ const PROPOSE_HEIGHT = 340;
25
+ /** The hours the day view draws for an invitation, as `InviteTimeline` will (a fixed estimate is all a popover's box needs). */
26
+ function estimateHeight(invite, proposing) {
27
+ const cap = window.innerHeight - WINDOW_MARGIN;
28
+ if (proposing) {
29
+ return Math.min(PROPOSE_HEIGHT, cap);
30
+ }
31
+ const start = invite.startDate ? new Date(invite.startDate) : undefined;
32
+ const end = invite.endDate ? new Date(invite.endDate) : undefined;
33
+ const spanHours = start && end ? Math.max(0, (end.getTime() - start.getTime()) / 3600000) : 1;
34
+ // The day view spans the invitation with two hours either side, and never less than five.
35
+ const hours = Math.min(24, Math.max(5, Math.ceil(spanHours) + 4));
36
+ const allDay = invite.schedule.filter((entry) => entry.allDay).length;
37
+ return Math.min(CHROME_HEIGHT + hours * HOURS_HEIGHT + allDay * CONFLICT_LINE_HEIGHT + (invite.conflicts.length > 0 ? CONFLICT_LINE_HEIGHT : 0), cap);
38
+ }
39
+ const RESPONSES = [
40
+ { response: "accepted", label: "Accept", text: "Accept", failure: "Couldn't accept this meeting" },
41
+ { response: "declined", label: "Decline", text: "Decline", failure: "Couldn't decline this meeting" },
42
+ { response: "tentative", label: "Tentative", text: "?", failure: "Couldn't respond tentatively to this meeting" },
43
+ ];
44
+ /** What is inside the RSVP popover (or dialog): the meeting, a day view around it and the buttons that answer it. */
45
+ function RsvpPanel({ messageUid, invite, onDone, onClose, onProposingChange }) {
46
+ const titleId = useId();
47
+ const [pending, setPending] = useState(null);
48
+ const [proposing, setProposingState] = useState(false);
49
+ const [menuOpen, setMenuOpen] = useState(false);
50
+ const rootRef = useRef(null);
51
+ const moreRef = useRef(null);
52
+ const menuRef = useRef(null);
53
+ const busy = pending !== null;
54
+ const when = formatInviteWhen(invite);
55
+ // Focus goes to the first thing to do when the panel opens.
56
+ useEffect(() => {
57
+ rootRef.current?.querySelector("button:not([disabled])")?.focus();
58
+ }, []);
59
+ function setProposing(next) {
60
+ setProposingState(next);
61
+ onProposingChange?.(next);
62
+ }
63
+ async function answer(response, failure) {
64
+ setPending(response);
65
+ try {
66
+ onDone(await respondToMessageInvite(messageUid, response));
67
+ }
68
+ catch (err) {
69
+ notifyApiError(err, failure);
70
+ setPending(null);
71
+ }
72
+ }
73
+ async function propose(proposal) {
74
+ setPending("propose");
75
+ try {
76
+ onDone(await proposeNewTime(messageUid, proposal));
77
+ }
78
+ catch (err) {
79
+ notifyApiError(err, "Couldn't send the proposed time");
80
+ setPending(null);
81
+ }
82
+ }
83
+ function handleKeyDown(event) {
84
+ if (event.key === "Escape") {
85
+ event.stopPropagation();
86
+ if (menuOpen) {
87
+ setMenuOpen(false);
88
+ moreRef.current?.focus();
89
+ }
90
+ else {
91
+ onClose();
92
+ }
93
+ }
94
+ }
95
+ function handleMenuKeyDown(event) {
96
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") {
97
+ return;
98
+ }
99
+ event.preventDefault();
100
+ const items = Array.from(menuRef.current.querySelectorAll('[role="menuitem"]'));
101
+ const at = items.indexOf(document.activeElement);
102
+ items[(at + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length].focus();
103
+ }
104
+ return (_jsxs("div", { ref: rootRef, role: "group", "aria-labelledby": titleId, tabIndex: -1, onKeyDown: handleKeyDown, className: "relative flex-1 min-h-0 flex flex-col text-sm text-text outline-none", children: [_jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto px-4 pt-3 pb-2 flex flex-col gap-2", children: [_jsx("h3", { id: titleId, className: "font-semibold break-words", children: invite.summary?.trim() || "(no title)" }), when && _jsxs("p", { className: "break-words", children: [when, invite.recurring && _jsx("span", { className: "text-text-muted", children: " (Repeats)" })] }), invite.conflicts.length > 0 && (_jsxs("p", { className: "text-danger break-words", children: [_jsx("span", { className: "font-medium", children: "Conflicts with:" }), " ", conflictSummary(invite.conflicts)] })), proposing ? (_jsx(ProposeTimeForm, { invite: invite, busy: pending === "propose", onSubmit: (proposal) => void propose(proposal), onCancel: () => setProposing(false) })) : (_jsx(InviteTimeline, { invite: invite }))] }), !proposing && (_jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-t border-border", "aria-busy": busy || undefined, children: [RESPONSES.map(({ response, label, text, failure }) => {
105
+ const current = invite.response === response;
106
+ return (_jsxs(Button, { type: "button", variant: response === "accepted" && !current ? "primary" : "secondary", className: ["!w-auto", response === "tentative" ? "!px-3" : "", current ? "!bg-primary/10 !border-primary disabled:!opacity-100" : ""].join(" "), "aria-label": label, title: label, "aria-pressed": current, "aria-busy": pending === response || undefined, loading: pending === response, disabled: busy || current, onClick: () => void answer(response, failure), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), text] }, response));
107
+ }), _jsxs("div", { className: "ml-auto relative", children: [_jsx("button", { ref: moreRef, type: "button", "aria-label": "More actions", "aria-haspopup": "menu", "aria-expanded": menuOpen, disabled: busy, onClick: () => setMenuOpen((open) => !open), className: "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-text hover:bg-surface-alt disabled:opacity-50", children: _jsx(HiOutlineEllipsisHorizontal, { size: 18, "aria-hidden": "true" }) }), menuOpen && (_jsxs("div", { ref: (node) => {
108
+ menuRef.current = node;
109
+ // A menu that has just opened takes the focus, as a menu does.
110
+ node?.querySelector('[role="menuitem"]')?.focus();
111
+ }, role: "menu", "aria-label": "More actions", onKeyDown: handleMenuKeyDown, className: "absolute bottom-full right-0 mb-1 w-48 py-1 rounded-md border border-border bg-surface shadow-modal z-10", children: [invite.canPropose && (_jsx("button", { type: "button", role: "menuitem", onClick: () => {
112
+ setMenuOpen(false);
113
+ setProposing(true);
114
+ }, className: "w-full px-3 py-2 text-left hover:bg-surface-alt", children: "Propose new time" })), _jsx("a", { role: "menuitem", href: calendarHref(invite), className: "block px-3 py-2 hover:bg-surface-alt", children: "Open in calendar" })] }))] })] }))] }));
115
+ }
116
+ /**
117
+ * What the RSVP button opens: an anchored popover on a desktop, a dialog on a phone. A day view of the reader's schedule around the meeting, with
118
+ * the invitation highlighted and its conflicts marked, and Accept, Decline, a "?" for Tentative and a "..." menu (Propose new time, Open in calendar).
119
+ * An answer is sent to the server, which puts the meeting on the calendar and replies to the organizer; the popover then closes and hands the
120
+ * updated invitation to its caller. A failure raises a pop-up and leaves the popover open.
121
+ *
122
+ * Focus moves to the first button when it opens, Escape closes it (a menu open inside it first), and focus returns to the RSVP button.
123
+ */
124
+ export default function InviteRsvpPopover({ messageUid, invite, anchorRef, onDone, onClose }) {
125
+ const isMobile = useIsMobile();
126
+ const [proposing, setProposing] = useState(false);
127
+ const title = invite.summary?.trim() || "(no title)";
128
+ function close() {
129
+ onClose();
130
+ anchorRef.current?.focus();
131
+ }
132
+ function done(updated) {
133
+ onDone(updated);
134
+ close();
135
+ }
136
+ const panel = _jsx(RsvpPanel, { messageUid: messageUid, invite: invite, onDone: done, onClose: close, onProposingChange: setProposing });
137
+ if (isMobile) {
138
+ return (_jsx(Modal, { open: true, onClose: close, title: "Meeting invitation", children: panel }));
139
+ }
140
+ return (_jsx(PopoverPortal, { anchorRef: anchorRef, onClose: onClose, width: WIDTH, height: estimateHeight(invite, proposing), "aria-label": `RSVP: ${title}`, children: panel }));
141
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import type { MessageInvite } from "@rapidmx/react-shared/calendar/inviteApi.js";
3
+ /** A block of the day and where it sits among the blocks it overlaps. */
4
+ export interface PlacedBlock {
5
+ /** Which of the overlapping columns it is drawn in. */
6
+ column: number;
7
+ /** How many columns its cluster of overlapping blocks needs. */
8
+ columns: number;
9
+ }
10
+ /**
11
+ * Lays overlapping blocks side by side: blocks that overlap (directly or through a chain of others) form a cluster, and each block takes the first
12
+ * column of the cluster that is free where it starts. `blocks` must be sorted by start; the result is in the same order.
13
+ */
14
+ export declare function placeBlocks(blocks: {
15
+ start: number;
16
+ end: number;
17
+ }[]): PlacedBlock[];
18
+ export interface InviteTimelineProps {
19
+ invite: Pick<MessageInvite, "startDate" | "endDate" | "summary" | "conflicts" | "schedule">;
20
+ }
21
+ /**
22
+ * A small day agenda around an invitation, as Outlook draws it in its RSVP card: hour rows for the invitation's day (from a couple of hours before it
23
+ * to a couple after), the invitation itself as a highlighted block, and the reader's other events as blocks - the ones that overlap it marked in red
24
+ * (and named as conflicts to a screen reader). Overlapping blocks stand side by side. All-day events are listed above the hours rather than drawn.
25
+ * Draws nothing when the invitation has no usable time.
26
+ */
27
+ export default function InviteTimeline({ invite }: InviteTimelineProps): React.JSX.Element | null;