@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
@@ -3,16 +3,19 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
6
+ import { flushSync } from "react-dom";
6
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
8
  import { Attachment, Folder, Message, listAttachments } from "@rapidmx/react-shared/mail/mailApi.js";
8
9
  import { ConversationSummary, listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
9
10
  import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
10
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
12
  import MessageDetailPane from "./MessageDetailPane.js";
12
- import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
13
+ import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
13
14
  import { CollapsedCard, SkeletonCards, SubjectCard } from "./reading/MessageCard.js";
15
+ import PendingMessageCard from "./reading/PendingMessageCard.js";
14
16
  import { useMailShell } from "./layout/MailShell.js";
15
17
  import { setReadState } from "../../mail/messageReadState.js";
18
+ import { adoptedOutgoing, belongsToThread, settleOutgoing, useOutgoingReplies } from "../../mail/outbox/outgoingReplies.js";
16
19
  import { dateClass, isUnread, senderClass } from "./unreadStyle.js";
17
20
 
18
21
  /** One request's worth of the thread. The server's own default for `listConversationMessages()`. */
@@ -25,8 +28,12 @@ export const THREAD_PAGE_SIZE = 100;
25
28
  */
26
29
  export const THREAD_MESSAGE_LIMIT = 500;
27
30
 
31
+ /** What the pane needs to know of a conversation up front: the thread itself is loaded by `conversationId`. The subject and
32
+ * count fill the header while it loads, so a caller that only has a message (the mobile route) gives its subject and a count of 1. */
33
+ export type ConversationThreadHead = Pick<ConversationSummary, "conversationId" | "subject" | "messageCount">;
34
+
28
35
  export interface ConversationThreadPaneProps {
29
- conversation: ConversationSummary | null;
36
+ conversation: ConversationThreadHead | null;
30
37
  /** The mailbox the conversation was listed from - `listConversationMessages()` is mailbox-scoped. */
31
38
  mailboxUid: string;
32
39
  /**
@@ -117,6 +124,21 @@ async function loadThread(mailboxUid: string, conversationId: string): Promise<{
117
124
  return { messages, truncated: more };
118
125
  }
119
126
 
127
+ /**
128
+ * `next` - the thread as it was just read again - for the pane to show in place of `previous`. A message the pane holds a newer copy of (one that
129
+ * was read, flagged or labelled here a moment ago, whose change the read may have started before) keeps that copy, and a read that changed
130
+ * nothing gives back `previous` itself, so the pane is not drawn again.
131
+ */
132
+ function mergeThread(previous: Message[], next: Message[]): Message[] {
133
+ const held = new Map(previous.map((message) => [message.uid, message]));
134
+ const merged = next.map((message) => {
135
+ const own = held.get(message.uid);
136
+ return own && own.version > message.version ? own : message;
137
+ });
138
+ const unchanged = merged.length === previous.length && merged.every((message, index) => message.uid === previous[index].uid && message.version === previous[index].version);
139
+ return unchanged ? previous : merged;
140
+ }
141
+
120
142
  /** Whether the browser is showing a focus ring on `element` - it was focused from the keyboard (or by a key press's handler), not by a click. */
121
143
  function hasFocusRing(element: HTMLElement): boolean {
122
144
  try {
@@ -156,7 +178,9 @@ export default function ConversationThreadPane({
156
178
  onFolderCreated,
157
179
  shortcuts,
158
180
  }: ConversationThreadPaneProps) {
159
- const { trackMessageChange } = useMailShell();
181
+ const { trackMessageChange, live } = useMailShell();
182
+ // The replies and forwards this tab has sent, drawn at the top of the thread they continue until the server's own copy is in it.
183
+ const outgoing = useOutgoingReplies();
160
184
  const [messages, setMessages] = useState<Message[]>([]);
161
185
  const [attachmentsByUid, setAttachmentsByUid] = useState<Record<string, Attachment[]>>({});
162
186
  const [expandedUids, setExpandedUids] = useState<Set<string>>(new Set());
@@ -179,13 +203,19 @@ export default function ConversationThreadPane({
179
203
  * expansion run below must sit that render out rather than anchor on a message from another thread. */
180
204
  const loadedIdRef = useRef<string | null>(null);
181
205
  const rowRefs = useRef<Record<string, HTMLLIElement | null>>({});
182
- const headerRefs = useRef<Record<string, HTMLButtonElement | null>>({});
206
+ const headerRefs = useRef<Record<string, HTMLElement | null>>({});
183
207
  /** Where the toggled message's header sat in the viewport before it expanded, so the run below can put
184
208
  * it back there - expanding a message above the one being read must not shove that one off-screen. */
185
209
  const anchorRef = useRef<{ uid: string; top: number } | null>(null);
186
210
  /** The message whose header button had the focus when it was toggled: expanding or collapsing swaps that button for the other state's, so the
187
211
  * focus is put back on the new one. */
188
212
  const refocusRef = useRef<string | null>(null);
213
+ /** The pending cards that have already been scrolled to, so that only a message that has just been sent takes the view and the focus. */
214
+ const announcedRef = useRef<Set<string>>(new Set());
215
+ /** Messages that left the thread here (archived, moved, a scheduled send taken back): a read of the thread again still lists them, since a conversation spans folders. */
216
+ const removedRef = useRef<Set<string>>(new Set());
217
+ /** The last live update this pane has answered - one already in the shell's hands when the pane opened is not news. */
218
+ const seenLiveRef = useRef(live);
189
219
 
190
220
  const conversationId = conversation?.conversationId;
191
221
 
@@ -193,6 +223,8 @@ export default function ConversationThreadPane({
193
223
  const generation = ++generationRef.current;
194
224
  markReadRequestedRef.current = new Set();
195
225
  attachmentsRequestedRef.current = new Set();
226
+ announcedRef.current = new Set();
227
+ removedRef.current = new Set();
196
228
  appliedSelectionRef.current = null;
197
229
  loadedIdRef.current = null;
198
230
  setMessages([]);
@@ -212,6 +244,8 @@ export default function ConversationThreadPane({
212
244
  .then((loaded) => {
213
245
  if (generation !== generationRef.current) return;
214
246
  loadedIdRef.current = conversationId;
247
+ // A message sent from here whose Sent Items copy is already in the thread is drawn as that copy, not as a pending card.
248
+ settleOutgoing(mailboxUid, loaded.messages);
215
249
  setMessages(loaded.messages);
216
250
  setTruncated(loaded.truncated);
217
251
  })
@@ -334,6 +368,80 @@ export default function ConversationThreadPane({
334
368
  }
335
369
  }, [expandedUids, messages]);
336
370
 
371
+ /**
372
+ * Reads the thread again, quietly: what is on screen stays until the answer is here (a read that fails changes nothing), a message that arrived
373
+ * is added at the top, and the reader's expanded and collapsed cards are left as they are. A message this tab sent - which the server has
374
+ * now filed in Sent Items under the same `uid` - takes the place of its pending card, expanded as that card was, with the focus if the
375
+ * card had it. A thread that has not finished its first load is not read again: that load is the fresh read.
376
+ */
377
+ function refreshThread() {
378
+ const id = loadedIdRef.current;
379
+ if (!id) {
380
+ return;
381
+ }
382
+ const generation = generationRef.current;
383
+ loadThread(mailboxUid, id).then(
384
+ (loaded) => {
385
+ if (generation !== generationRef.current) return;
386
+ const current = loaded.messages.filter((message) => !removedRef.current.has(message.uid));
387
+ const adopted = adoptedOutgoing(mailboxUid, current);
388
+ const refocus = adopted.find((uid) => document.activeElement === headerRefs.current[uid]);
389
+ // Drawn first, and only then is the pending card forgotten: there is never a frame with neither it nor the real message.
390
+ flushSync(() => {
391
+ setMessages((previous) => mergeThread(previous, current));
392
+ setTruncated(loaded.truncated);
393
+ if (adopted.length > 0) {
394
+ setExpandedUids((previous) => new Set([...previous, ...adopted]));
395
+ }
396
+ if (refocus) {
397
+ setPendingFocusUid(refocus);
398
+ }
399
+ });
400
+ settleOutgoing(mailboxUid, current);
401
+ },
402
+ // Quietly: the next live update reads it again, and what is shown is still right.
403
+ () => undefined,
404
+ );
405
+ }
406
+
407
+ // Another message of this conversation may have arrived - a recipient's reply, one sent from another tab or device, or this tab's own reply
408
+ // filed in Sent Items - whenever the shell announces a live update that touched one of this mailbox's folders (or does not say which).
409
+ useEffect(() => {
410
+ if (live === seenLiveRef.current) return;
411
+ seenLiveRef.current = live;
412
+ if (live.folderUids === null || folders.some((folder) => live.folderUids!.has(folder.uid))) {
413
+ refreshThread();
414
+ }
415
+ }, [live]);
416
+
417
+ // The server has relayed a message this tab sent: its Sent Items copy is read for at once, without waiting for the live update that follows.
418
+ const sentKey = outgoing
419
+ .filter((reply) => reply.state === "sent" && reply.mailboxUid === mailboxUid)
420
+ .map((reply) => reply.uid)
421
+ .join(",");
422
+ useEffect(() => {
423
+ if (sentKey) {
424
+ refreshThread();
425
+ }
426
+ }, [sentKey]);
427
+
428
+ // The messages sent from here that continue this thread and whose real copy it does not hold yet, newest first (the pane's order). A message
429
+ // that replies to nothing here - a new message, or a reply to another conversation - is not in this list, and never is.
430
+ const pendingCards = outgoing
431
+ .filter((reply) => reply.mailboxUid === mailboxUid && !messages.some((message) => message.uid === reply.uid) && belongsToThread(reply, messages))
432
+ .reverse();
433
+ // A message that has just been sent is scrolled into view and takes the focus - the compose window it was sent from has just closed, and the focus
434
+ // with it - once. A failed one, or one that was already there when the thread was opened, is left where it is.
435
+ const pendingKey = pendingCards.map((reply) => reply.uid).join(",");
436
+ useEffect(() => {
437
+ const fresh = pendingCards.filter((reply) => reply.state === "sending" && !announcedRef.current.has(reply.uid));
438
+ if (fresh.length === 0) return;
439
+ for (const reply of fresh) {
440
+ announcedRef.current.add(reply.uid);
441
+ }
442
+ setPendingFocusUid(fresh[0].uid);
443
+ }, [pendingKey]);
444
+
337
445
  /** A newer copy of one of the thread's messages, kept here and handed to the list - with the copy it
338
446
  * replaces where the caller was given one, so a conversation row can tell what actually changed. */
339
447
  function patchMessage(updated: Message, previous?: Message) {
@@ -343,6 +451,7 @@ export default function ConversationThreadPane({
343
451
 
344
452
  /** A message that left the folder being listed - it leaves the thread too, as it left the list. */
345
453
  function removeMessage(updated: Message) {
454
+ removedRef.current.add(updated.uid);
346
455
  setMessages((prev) => prev.filter((message) => message.uid !== updated.uid));
347
456
  onMessageRemoved(updated);
348
457
  }
@@ -395,7 +504,7 @@ export default function ConversationThreadPane({
395
504
  : undefined
396
505
  : error
397
506
  ? undefined
398
- : `${messages.length} message${messages.length === 1 ? "" : "s"}`
507
+ : `${messages.length + pendingCards.length} message${messages.length + pendingCards.length === 1 ? "" : "s"}`
399
508
  }
400
509
  >
401
510
  {truncated && !loading && !error && (
@@ -413,6 +522,22 @@ export default function ConversationThreadPane({
413
522
  </div>
414
523
  ) : (
415
524
  <ul className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">
525
+ {pendingCards.map((reply) => (
526
+ <li
527
+ key={reply.uid}
528
+ ref={(node) => {
529
+ rowRefs.current[reply.uid] = node;
530
+ }}
531
+ data-outgoing="true"
532
+ >
533
+ <PendingMessageCard
534
+ reply={reply}
535
+ headerRef={(node) => {
536
+ headerRefs.current[reply.uid] = node;
537
+ }}
538
+ />
539
+ </li>
540
+ ))}
416
541
  {messages.map((message) => {
417
542
  const uid = message.uid;
418
543
  const expanded = expandedUids.has(uid);
@@ -0,0 +1,387 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { useId, useState } from "react";
6
+ import { HiOutlineCalendarDays, HiOutlineCheck } from "react-icons/hi2";
7
+ import {
8
+ InviteParticipant,
9
+ InviteResponse,
10
+ MessageInvite,
11
+ ProposedTime,
12
+ acceptProposal,
13
+ proposeNewTime,
14
+ removeMessageInvite,
15
+ respondToMessageInvite,
16
+ } from "@rapidmx/react-shared/calendar/inviteApi.js";
17
+ import { guestPermissionsOf } from "@rapidmx/react-shared/calendar/calendarApi.js";
18
+ import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
19
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
20
+ import EventDescriptionView from "../calendar/EventDescriptionView.js";
21
+ import RequestChangeForm from "../calendar/RequestChangeForm.js";
22
+ import { VISIBILITY_LABEL, describeGuestPermissions } from "../calendar/eventFormat.js";
23
+ import { notifyApiError } from "../../notifications/apiErrors.js";
24
+ import { calendarHref, conflictSummary, formatInviteWhen } from "./invite/inviteFormat.js";
25
+ import { useMessageInvite } from "./invite/inviteStore.js";
26
+ import ProposeTimeForm from "./invite/ProposeTimeForm.js";
27
+
28
+ export { formatInviteWhen, isCalendarAttachment } from "./invite/inviteFormat.js";
29
+
30
+ const HEADINGS: Record<string, string> = {
31
+ REQUEST: "Meeting invitation",
32
+ CANCEL: "Meeting canceled",
33
+ REPLY: "Meeting response",
34
+ COUNTER: "New time proposed",
35
+ };
36
+
37
+ /** The heading of a `COUNTER` that is a guest's request for a change (rather than a different time). */
38
+ const CHANGE_REQUEST_HEADING = "Change requested";
39
+
40
+ /** How an answer reads in "Bob accepted." (a REPLY) - a participant who has not answered has not answered. */
41
+ const REPLY_VERBS: Partial<Record<NonNullable<InviteParticipant["responseStatus"]>, string>> = {
42
+ accepted: "accepted",
43
+ tentative: "tentatively accepted",
44
+ declined: "declined",
45
+ };
46
+
47
+ /** The reader's own answer, as the line under the invitation says it. */
48
+ const ANSWER_TEXT: Record<InviteResponse, string> = {
49
+ accepted: "You accepted this meeting.",
50
+ tentative: "You tentatively accepted this meeting.",
51
+ declined: "You declined this meeting. It is not on your calendar.",
52
+ };
53
+
54
+ const ANSWER_CLASS: Record<InviteResponse, string> = {
55
+ accepted: "text-success",
56
+ tentative: "text-text",
57
+ declined: "text-danger",
58
+ };
59
+
60
+ const RESPONSES: { response: InviteResponse; label: string; failure: string }[] = [
61
+ { response: "accepted", label: "Accept", failure: "Couldn't accept this meeting" },
62
+ { response: "tentative", label: "Tentative", failure: "Couldn't respond tentatively to this meeting" },
63
+ { response: "declined", label: "Decline", failure: "Couldn't decline this meeting" },
64
+ ];
65
+
66
+ /** The most attendees that are listed by name; a longer list is only counted. */
67
+ const MAX_NAMED_ATTENDEES = 5;
68
+
69
+ /** What the card is doing for the reader right now: an answer being sent, an add/remove, a proposal, an accepted proposal. */
70
+ type Pending = InviteResponse | "add" | "remove" | "propose" | "acceptProposal";
71
+
72
+ function attendeeSummary(attendees: InviteParticipant[]): string | undefined {
73
+ if (attendees.length === 0) {
74
+ return undefined;
75
+ }
76
+ const count = `${attendees.length} ${attendees.length === 1 ? "attendee" : "attendees"}`;
77
+ return attendees.length <= MAX_NAMED_ATTENDEES ? `${count}: ${attendees.map((a) => a.displayName || a.address).join(", ")}` : count;
78
+ }
79
+
80
+ export interface InviteCardProps {
81
+ /** The message whose calendar invitation to show, if it carries one. Nothing is drawn until the server says there is one. */
82
+ messageUid: string;
83
+ /** The level of the card's heading, so it sits below whatever heading its surroundings have (`2` in the reading pane, `3` inside a thread's card). */
84
+ headingLevel?: 2 | 3;
85
+ }
86
+
87
+ /**
88
+ * The calendar invitation a message carries, as Outlook and Gmail draw it: what the meeting is, when and where, who organizes it, and the buttons
89
+ * that answer it - Accept, Tentative, Decline (and Propose new time) for a request; "Add to calendar" for a published event; "Remove from
90
+ * calendar" for a cancellation; "Accept proposal" for an attendee's proposed new time. An attendee's answer to the reader's own invitation is
91
+ * one line ("Bob tentatively accepted."). Everything that follows an action (the meeting going onto the calendar or off it, the reply to the
92
+ * organizer) is the server's; this only asks for it and draws the invitation the server sends back.
93
+ *
94
+ * Asks the server once per message (through the shared invitation cache, so the list row's RSVP chip and this card agree). A message with no readable
95
+ * invitation, and a lookup that fails for any reason, draw nothing - a lookup that fails is never retried and never disturbs the message around it.
96
+ * An answer that arrives after the message changed or this was unmounted is dropped.
97
+ */
98
+ export default function InviteCard({ messageUid, headingLevel = 2 }: InviteCardProps) {
99
+ const { invite, setInvite } = useMessageInvite(messageUid);
100
+ const [pending, setPending] = useState<Pending | null>(null);
101
+ const [proposing, setProposing] = useState(false);
102
+ // Said once after a proposal went out: the invitation itself doesn't change.
103
+ const [proposed, setProposed] = useState(false);
104
+ // A guest's request for a change (or for guests to be added): the form is open, and once it went out.
105
+ const [requesting, setRequesting] = useState(false);
106
+ const [requested, setRequested] = useState(false);
107
+ const headingId = useId();
108
+
109
+ if (!invite) {
110
+ return null;
111
+ }
112
+
113
+ /** Runs one of the card's actions: the buttons are off while it does, its result replaces what the card shows, and a failure leaves the card as it was. */
114
+ async function run(action: Pending, failure: string, call: () => Promise<MessageInvite>): Promise<boolean> {
115
+ setPending(action);
116
+ try {
117
+ setInvite(await call());
118
+ return true;
119
+ } catch (err) {
120
+ notifyApiError(err, failure);
121
+ return false;
122
+ } finally {
123
+ setPending(null);
124
+ }
125
+ }
126
+
127
+ async function sendProposal(proposal: ProposedTime) {
128
+ if (await run("propose", "Couldn't send the proposed time", () => proposeNewTime(messageUid, proposal))) {
129
+ setProposing(false);
130
+ setProposed(true);
131
+ }
132
+ }
133
+
134
+ const busy = pending !== null;
135
+ const Heading = `h${headingLevel}` as const;
136
+ const method = invite.method.toUpperCase();
137
+ const isReply = method === "REPLY";
138
+ const isCounter = method === "COUNTER";
139
+ // A `COUNTER` a guest sent to change the event (not just its time): whether the organizer's server applied it already.
140
+ const changeRequest = isCounter ? invite.changeRequest : undefined;
141
+ const permissions = guestPermissionsOf(invite.guestPermissions ?? {});
142
+ const hiddenList = !invite.isOrganizer && !permissions.guestsCanSeeGuestList;
143
+ const canRequest = !!invite.calendarEventUid && (!!invite.canRequestChange || !!invite.canRequestInvite);
144
+ const title = invite.summary?.trim() || "(no title)";
145
+ const when = formatInviteWhen(invite);
146
+ const attendees = hiddenList ? "The organizer has hidden the guest list" : attendeeSummary(invite.attendees);
147
+ const hasDescription = !!(invite.descriptionHtml?.trim() || invite.description?.trim());
148
+ const visibility = invite.visibility && invite.visibility !== "default" ? VISIBILITY_LABEL[invite.visibility] : undefined;
149
+ const sender = invite.reply ?? invite.attendees[0];
150
+ const senderName = sender ? sender.displayName || sender.address : "Someone";
151
+ const showResponses = invite.canRespond && !invite.isOrganizer;
152
+ const conflicts = !isReply && !isCounter && method !== "CANCEL" && invite.conflicts.length > 0 ? conflictSummary(invite.conflicts) : undefined;
153
+ const hasActions =
154
+ showResponses ||
155
+ invite.canPropose ||
156
+ invite.canAcceptProposal ||
157
+ invite.canAdd ||
158
+ invite.canRemove ||
159
+ canRequest ||
160
+ (invite.onCalendar && !!invite.calendarEventUid);
161
+
162
+ return (
163
+ <section aria-labelledby={headingId} className="rounded-sm border border-border bg-surface-alt px-3 py-3 flex flex-col gap-2 text-sm text-text">
164
+ <Heading id={headingId} className="flex items-center gap-2 text-sm font-semibold">
165
+ <HiOutlineCalendarDays size={16} aria-hidden="true" className="shrink-0" />
166
+ {changeRequest ? CHANGE_REQUEST_HEADING : (HEADINGS[method] ?? "Calendar event")}
167
+ </Heading>
168
+ {isReply && (
169
+ // Someone answering the reader's own invitation: a line of news, nothing to do.
170
+ <p className="break-words">
171
+ <span className="font-semibold">{senderName}</span> {(sender?.responseStatus && REPLY_VERBS[sender.responseStatus]) ?? "responded"}.
172
+ </p>
173
+ )}
174
+ {isCounter && !changeRequest && (
175
+ <p className="break-words">
176
+ <span className="font-semibold">{senderName}</span> proposed a new time{when ? <>: {when}</> : "."}
177
+ </p>
178
+ )}
179
+ {changeRequest && (
180
+ <p className="break-words">
181
+ {changeRequest.applied ? (
182
+ <>
183
+ <span className="font-semibold">{senderName}</span>&rsquo;s change was applied.
184
+ </>
185
+ ) : (
186
+ <>
187
+ <span className="font-semibold">{senderName}</span> requested a change.
188
+ </>
189
+ )}
190
+ </p>
191
+ )}
192
+ <p className="font-medium break-words">{title}</p>
193
+ {!isReply && (
194
+ <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1">
195
+ {when && (!isCounter || !!changeRequest) && (
196
+ <>
197
+ <dt className="text-text-muted">When</dt>
198
+ <dd className="break-words">
199
+ {when}
200
+ {invite.recurring && <span className="text-text-muted"> (Repeats)</span>}
201
+ </dd>
202
+ </>
203
+ )}
204
+ {invite.location && (
205
+ <>
206
+ <dt className="text-text-muted">Where</dt>
207
+ <dd className="break-words">{invite.location}</dd>
208
+ </>
209
+ )}
210
+ {hasDescription && (!isCounter || !!changeRequest) && (
211
+ <>
212
+ <dt className="text-text-muted">Description</dt>
213
+ <dd className="break-words">
214
+ <EventDescriptionView html={invite.descriptionHtml} text={invite.description} />
215
+ </dd>
216
+ </>
217
+ )}
218
+ {invite.organizer && !isCounter && (
219
+ <>
220
+ <dt className="text-text-muted">Organizer</dt>
221
+ <dd className="break-words">{formatMailAddress(invite.organizer)}</dd>
222
+ </>
223
+ )}
224
+ {attendees && (!isCounter || !!changeRequest) && (
225
+ <>
226
+ <dt className="text-text-muted">Attendees</dt>
227
+ <dd className="break-words">{attendees}</dd>
228
+ </>
229
+ )}
230
+ {visibility && !isCounter && (
231
+ <>
232
+ <dt className="text-text-muted">Visibility</dt>
233
+ <dd className="break-words">{visibility}</dd>
234
+ </>
235
+ )}
236
+ </dl>
237
+ )}
238
+ {isReply && when && <p className="text-text-muted break-words">{when}</p>}
239
+ {conflicts && (
240
+ <p className="text-danger break-words">
241
+ <span className="font-medium">Conflicts with:</span> {conflicts}
242
+ </p>
243
+ )}
244
+ {invite.guestPermissions && !isReply && !isCounter && method !== "CANCEL" && (
245
+ <p className="text-xs text-text-muted break-words">{describeGuestPermissions(permissions)}</p>
246
+ )}
247
+ {invite.isOrganizer && !isReply && !isCounter && <p className="text-text-muted">You are the organizer of this meeting.</p>}
248
+ {method === "CANCEL" && <p className="font-medium text-danger">This meeting was canceled.</p>}
249
+ {invite.response && !invite.isOrganizer && !isReply && !isCounter && (
250
+ <p role="status" className={`font-medium ${ANSWER_CLASS[invite.response]}`}>
251
+ {ANSWER_TEXT[invite.response]}
252
+ </p>
253
+ )}
254
+ {isCounter && invite.response === "accepted" && !changeRequest?.applied && (
255
+ <p role="status" className="font-medium text-success">
256
+ You accepted the proposed time.
257
+ </p>
258
+ )}
259
+ {proposed && (
260
+ <p role="status" className="font-medium text-text">
261
+ Your proposed time was sent to the organizer.
262
+ </p>
263
+ )}
264
+ {requested && (
265
+ <p role="status" className="font-medium text-text">
266
+ Your change was sent to the organizer.
267
+ </p>
268
+ )}
269
+ {invite.outdated && <p className="text-xs text-text-muted">A newer version of this meeting is already on your calendar.</p>}
270
+ {hasActions && (
271
+ <div className="flex flex-wrap items-center gap-2">
272
+ {showResponses && (
273
+ <div role="group" aria-label="Respond to this meeting" aria-busy={busy || undefined} className="flex flex-wrap gap-2">
274
+ {RESPONSES.map(({ response, label, failure }) => {
275
+ const current = invite.response === response;
276
+ return (
277
+ <Button
278
+ key={response}
279
+ type="button"
280
+ variant="secondary"
281
+ className={["!w-auto", current ? "!bg-primary/10 !border-primary disabled:!opacity-100" : ""].join(" ")}
282
+ aria-pressed={current}
283
+ aria-busy={pending === response || undefined}
284
+ loading={pending === response}
285
+ disabled={busy || current}
286
+ onClick={() => void run(response, failure, () => respondToMessageInvite(messageUid, response))}
287
+ >
288
+ {current && <HiOutlineCheck size={14} aria-hidden="true" />}
289
+ {label}
290
+ </Button>
291
+ );
292
+ })}
293
+ </div>
294
+ )}
295
+ {invite.canPropose && !proposing && (
296
+ <Button type="button" variant="text" disabled={busy} onClick={() => setProposing(true)}>
297
+ Propose new time
298
+ </Button>
299
+ )}
300
+ {canRequest && !requesting && (
301
+ <Button type="button" variant="secondary" className="!w-auto" disabled={busy} onClick={() => setRequesting(true)}>
302
+ {invite.canRequestChange ? "Request a change" : "Add guests"}
303
+ </Button>
304
+ )}
305
+ {invite.canAcceptProposal && invite.response !== "accepted" && (
306
+ <Button
307
+ type="button"
308
+ variant="secondary"
309
+ className="!w-auto"
310
+ aria-busy={pending === "acceptProposal" || undefined}
311
+ loading={pending === "acceptProposal"}
312
+ disabled={busy}
313
+ onClick={() => void run("acceptProposal", "Couldn't accept the proposed time", () => acceptProposal(messageUid))}
314
+ >
315
+ Accept proposal
316
+ </Button>
317
+ )}
318
+ {invite.canAdd && (
319
+ <Button
320
+ type="button"
321
+ variant="secondary"
322
+ className="!w-auto"
323
+ aria-busy={pending === "add" || undefined}
324
+ loading={pending === "add"}
325
+ disabled={busy}
326
+ // A published event has no organizer to answer, so adding it is accepting it: the server sends no reply.
327
+ onClick={() => void run("add", "Couldn't add this event to your calendar", () => respondToMessageInvite(messageUid, "accepted"))}
328
+ >
329
+ Add to calendar
330
+ </Button>
331
+ )}
332
+ {invite.canRemove && (
333
+ <Button
334
+ type="button"
335
+ variant="secondary"
336
+ className="!w-auto"
337
+ aria-busy={pending === "remove" || undefined}
338
+ loading={pending === "remove"}
339
+ disabled={busy}
340
+ onClick={() => void run("remove", "Couldn't remove this meeting from your calendar", () => removeMessageInvite(messageUid))}
341
+ >
342
+ Remove from calendar
343
+ </Button>
344
+ )}
345
+ {invite.onCalendar && invite.calendarEventUid && (
346
+ <a href={calendarHref(invite)} className="text-sm font-medium text-primary-dark hover:underline">
347
+ Open in Calendar
348
+ </a>
349
+ )}
350
+ </div>
351
+ )}
352
+ {requesting && (
353
+ <div className="rounded-md bg-surface p-3">
354
+ <RequestChangeForm
355
+ subject={{
356
+ uid: invite.calendarEventUid as string,
357
+ title: invite.summary ?? "",
358
+ location: invite.location,
359
+ startDate: invite.startDate,
360
+ endDate: invite.endDate,
361
+ allDay: invite.allDay,
362
+ recurring: invite.recurring,
363
+ description: invite.description,
364
+ descriptionHtml: invite.descriptionHtml,
365
+ guestAddresses: invite.attendees.map((attendee) => attendee.address),
366
+ }}
367
+ canChange={!!invite.canRequestChange}
368
+ canInvite={!!invite.canRequestInvite}
369
+ onSent={() => {
370
+ setRequesting(false);
371
+ setRequested(true);
372
+ }}
373
+ onCancel={() => setRequesting(false)}
374
+ />
375
+ </div>
376
+ )}
377
+ {proposing && (
378
+ <ProposeTimeForm
379
+ invite={invite}
380
+ busy={pending === "propose"}
381
+ onSubmit={(proposal) => void sendProposal(proposal)}
382
+ onCancel={() => setProposing(false)}
383
+ />
384
+ )}
385
+ </section>
386
+ );
387
+ }