@rapidmx/web-client 0.30.0 → 0.32.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 (402) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +3 -3
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +2 -2
  33. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  34. package/apps/shared/components/admin/elevation.ts +19 -0
  35. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  36. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  37. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  38. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  40. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  41. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  42. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  43. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  44. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  45. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  46. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  47. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  48. package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
  49. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  50. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  51. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  52. package/apps/shared/components/admin/signOut.ts +3 -0
  53. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  54. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  55. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  56. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  57. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  58. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  59. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  60. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  61. package/apps/shared/components/calendar/ListView.tsx +99 -25
  62. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  63. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  64. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  65. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  66. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  67. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  68. package/apps/shared/components/calendar/timePicker.ts +5 -2
  69. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  70. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  71. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  72. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  73. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  74. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  75. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  76. package/apps/shared/components/layout/AppShell.tsx +9 -0
  77. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  78. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  79. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  80. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  81. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  82. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  83. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  84. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  85. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  86. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  87. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  88. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  89. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  90. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  91. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  92. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  93. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  94. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  95. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  96. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  97. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  99. package/apps/shared/components/tasks/dueDate.ts +29 -0
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  101. package/apps/shared/mail/decryptedMessages.ts +51 -2
  102. package/apps/shared/mail/listSnapshots.ts +2 -0
  103. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  104. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  105. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  106. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  107. package/apps/shared/notifications/apiErrors.ts +2 -0
  108. package/apps/shared/notifications/preferences.ts +1 -1
  109. package/apps/shared/notifications/store.ts +3 -1
  110. package/apps/shared/plugins/pluginNav.ts +4 -2
  111. package/apps/www/calendar/index.tsx +24 -12
  112. package/apps/www/contacts/[uid].tsx +6 -1
  113. package/apps/www/contacts/index.tsx +66 -12
  114. package/apps/www/index.tsx +91 -28
  115. package/apps/www/messages/[uid].tsx +8 -3
  116. package/apps/www/settings/auto-reply/index.tsx +13 -0
  117. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  118. package/apps/www/settings/encryption/index.tsx +66 -4
  119. package/apps/www/settings/privacy/index.tsx +4 -1
  120. package/apps/www/settings/profile/index.tsx +12 -1
  121. package/apps/www/settings/sharing/index.tsx +11 -1
  122. package/apps/www/settings/signatures/[uid].tsx +6 -4
  123. package/apps/www/tasks/index.tsx +97 -29
  124. package/dist/apps/admin/audit-log/index.js +3 -3
  125. package/dist/apps/admin/branding/index.js +3 -3
  126. package/dist/apps/admin/data-requests/index.js +17 -7
  127. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  128. package/dist/apps/admin/distribution-lists/index.js +3 -3
  129. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  130. package/dist/apps/admin/domains/[uid].js +6 -5
  131. package/dist/apps/admin/domains/index.js +3 -3
  132. package/dist/apps/admin/domains/new/index.js +3 -3
  133. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  134. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  135. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  136. package/dist/apps/admin/index.js +4 -4
  137. package/dist/apps/admin/ingest-queue/index.js +3 -3
  138. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  139. package/dist/apps/admin/quarantine/index.js +4 -4
  140. package/dist/apps/admin/retention-policy/index.js +3 -3
  141. package/dist/apps/admin/signing-certificates/index.js +3 -3
  142. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  143. package/dist/apps/admin/transport-rules/index.js +3 -3
  144. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  145. package/dist/apps/escrow/matters/[uid].js +8 -3
  146. package/dist/apps/escrow/matters/new/index.js +4 -0
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  148. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  149. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  151. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  152. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
  153. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  156. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  157. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
  158. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  159. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  160. package/dist/apps/shared/components/admin/elevation.js +16 -0
  161. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  162. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  163. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  164. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  165. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  166. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  167. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  168. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  169. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  170. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  171. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  172. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  174. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
  176. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  177. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  178. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  179. package/dist/apps/shared/components/admin/signOut.js +3 -0
  180. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  181. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  182. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  183. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  184. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  185. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  186. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  187. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  188. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  189. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  190. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  191. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  192. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  193. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  194. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  195. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  196. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  197. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  198. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  199. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  201. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  202. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  203. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  204. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  205. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  206. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  207. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  208. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  209. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  210. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  211. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  212. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  213. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  214. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  215. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  216. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  217. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  218. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  219. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  221. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  222. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  224. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  226. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  227. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  228. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  229. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  230. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  231. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  232. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  233. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  234. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  235. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  236. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  237. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  238. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  239. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  240. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  241. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  242. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  243. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  244. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  245. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  246. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  247. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  248. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  249. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  250. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  251. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  252. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  253. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  254. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  255. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  256. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  257. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  258. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  259. package/dist/apps/shared/notifications/preferences.js +1 -1
  260. package/dist/apps/shared/notifications/store.js +3 -1
  261. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  262. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  263. package/dist/apps/www/calendar/index.js +24 -12
  264. package/dist/apps/www/contacts/[uid].js +6 -2
  265. package/dist/apps/www/contacts/index.js +62 -11
  266. package/dist/apps/www/index.js +85 -30
  267. package/dist/apps/www/messages/[uid].js +8 -3
  268. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  269. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  270. package/dist/apps/www/settings/encryption/index.js +29 -5
  271. package/dist/apps/www/settings/privacy/index.js +3 -2
  272. package/dist/apps/www/settings/profile/index.js +4 -1
  273. package/dist/apps/www/settings/sharing/index.js +10 -1
  274. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  275. package/dist/apps/www/tasks/index.js +94 -32
  276. package/dist/lib/admin/auditLogApi.js +2 -2
  277. package/dist/lib/admin/distributionListsApi.js +2 -2
  278. package/dist/lib/admin/domainsApi.js +2 -2
  279. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  280. package/dist/lib/admin/escrowScopesApi.js +2 -2
  281. package/dist/lib/admin/pluginsApi.d.ts +31 -2
  282. package/dist/lib/admin/pluginsApi.js +23 -1
  283. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  284. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  285. package/dist/lib/admin/transportRulesApi.js +2 -2
  286. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  287. package/dist/lib/appearance/preferencesApi.js +3 -14
  288. package/dist/lib/auth/session.d.ts +51 -12
  289. package/dist/lib/auth/session.js +214 -38
  290. package/dist/lib/branding/brandingApi.d.ts +3 -3
  291. package/dist/lib/branding/brandingApi.js +9 -21
  292. package/dist/lib/calendar/calendarApi.js +3 -1
  293. package/dist/lib/calendar/eventSearch.js +43 -6
  294. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  295. package/dist/lib/calendar/freeBusyApi.js +13 -5
  296. package/dist/lib/calendar/recurrence.js +51 -3
  297. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  298. package/dist/lib/calendar/useDayKey.js +43 -0
  299. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  300. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  301. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  302. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  303. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  304. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  305. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  306. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  307. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  308. package/dist/lib/components/menus/MenuButton.js +219 -0
  309. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  310. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  311. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  312. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  313. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  314. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  315. package/dist/lib/contacts/contactsApi.js +16 -19
  316. package/dist/lib/contacts/gravatar.d.ts +14 -3
  317. package/dist/lib/contacts/gravatar.js +42 -5
  318. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  319. package/dist/lib/contacts/preparePhoto.js +250 -0
  320. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  321. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  322. package/dist/lib/contacts/vcard.d.ts +3 -0
  323. package/dist/lib/contacts/vcard.js +59 -7
  324. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  325. package/dist/lib/crypto/escrowKeys.js +20 -1
  326. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  327. package/dist/lib/crypto/messageSecurity.js +23 -5
  328. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  329. package/dist/lib/crypto/passwordUnlock.js +13 -2
  330. package/dist/lib/crypto/smime.js +12 -2
  331. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  332. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  333. package/dist/lib/mail/compose/quotedImages.js +35 -9
  334. package/dist/lib/mail/labelsApi.js +2 -2
  335. package/dist/lib/mail/mailApi.d.ts +8 -5
  336. package/dist/lib/mail/mailApi.js +73 -39
  337. package/dist/lib/mail/mailDetailHooks.js +11 -3
  338. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  339. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  340. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  341. package/dist/lib/mail/mailboxImportApi.js +3 -15
  342. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  343. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  344. package/dist/lib/search/queryGrammar.js +12 -4
  345. package/dist/lib/search/searchTier3.d.ts +6 -1
  346. package/dist/lib/search/searchTier3.js +54 -25
  347. package/dist/lib/tasks/tasksApi.js +3 -3
  348. package/dist/lib/util/api.d.ts +26 -4
  349. package/dist/lib/util/api.js +73 -16
  350. package/dist/lib/util/apiQuery.d.ts +11 -0
  351. package/dist/lib/util/apiQuery.js +11 -0
  352. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  353. package/dist/lib/util/settleWithLimit.js +25 -0
  354. package/lib/admin/auditLogApi.ts +2 -2
  355. package/lib/admin/distributionListsApi.ts +2 -2
  356. package/lib/admin/domainsApi.ts +2 -2
  357. package/lib/admin/escrowAuditLogApi.ts +2 -2
  358. package/lib/admin/escrowScopesApi.ts +2 -2
  359. package/lib/admin/pluginsApi.ts +45 -3
  360. package/lib/admin/retentionPolicyApi.ts +4 -0
  361. package/lib/admin/transportRulesApi.ts +2 -2
  362. package/lib/appearance/preferencesApi.ts +3 -14
  363. package/lib/auth/session.ts +239 -39
  364. package/lib/branding/brandingApi.ts +9 -21
  365. package/lib/calendar/calendarApi.ts +3 -1
  366. package/lib/calendar/eventSearch.ts +46 -6
  367. package/lib/calendar/freeBusyApi.ts +14 -6
  368. package/lib/calendar/recurrence.ts +59 -8
  369. package/lib/calendar/useDayKey.ts +45 -0
  370. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  371. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  372. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  373. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  374. package/lib/components/menus/MenuButton.tsx +408 -0
  375. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  376. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  377. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  378. package/lib/contacts/contactsApi.ts +18 -19
  379. package/lib/contacts/gravatar.ts +47 -5
  380. package/lib/contacts/preparePhoto.ts +273 -0
  381. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  382. package/lib/contacts/vcard.ts +60 -7
  383. package/lib/crypto/escrowKeys.ts +21 -1
  384. package/lib/crypto/messageSecurity.ts +33 -9
  385. package/lib/crypto/passwordUnlock.ts +14 -2
  386. package/lib/crypto/smime.ts +13 -2
  387. package/lib/mail/compose/composeQuoting.ts +3 -2
  388. package/lib/mail/compose/quotedImages.ts +39 -9
  389. package/lib/mail/labelsApi.ts +2 -2
  390. package/lib/mail/mailApi.ts +96 -39
  391. package/lib/mail/mailDetailHooks.ts +11 -3
  392. package/lib/mail/mailFilterRulesApi.ts +2 -2
  393. package/lib/mail/mailSignaturesApi.ts +2 -2
  394. package/lib/mail/mailboxImportApi.ts +9 -15
  395. package/lib/mail/messageBodySanitizer.ts +47 -1
  396. package/lib/search/queryGrammar.ts +13 -4
  397. package/lib/search/searchTier3.ts +65 -32
  398. package/lib/tasks/tasksApi.ts +3 -3
  399. package/lib/util/api.ts +99 -16
  400. package/lib/util/apiQuery.ts +13 -0
  401. package/lib/util/settleWithLimit.ts +25 -0
  402. package/package.json +1 -1
@@ -34,11 +34,13 @@ function DetailRow({ icon, children }) {
34
34
  * An event this reader may only see as a busy block (`occurrence.redacted`: a private or confidential event on a shared calendar) is drawn as just that -
35
35
  * its time and that it is busy - with nothing to change, delete or answer.
36
36
  */
37
- export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }) {
37
+ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, canWrite = true, onClose, onModify, onSaved, onDeleted, }) {
38
38
  const client = useApiClient();
39
39
  const [error, setError] = useState(null);
40
40
  const [busy, setBusy] = useState(false);
41
41
  const [confirmingDelete, setConfirmingDelete] = useState(false);
42
+ // An answer to one occurrence of a series waits here for the reader to confirm: the server records it for the entire series.
43
+ const [pendingResponse, setPendingResponse] = useState(null);
42
44
  const [requesting, setRequesting] = useState(false);
43
45
  // The organizer's own join link for the event's video meeting: `undefined` while it is fetched, `null` once known to be none.
44
46
  const [organizerJoinUrl, setOrganizerJoinUrl] = useState(undefined);
@@ -84,6 +86,7 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
84
86
  // `@rapidmx/restapi`'s `respond()` route has no occurrence-vs-series scope of its own (see `calendarApi.ts`'s `respondToEvent` doc
85
87
  // comment) - it always acts on `occurrence.uid` as a single event document.
86
88
  async function handleRespond(responseStatus) {
89
+ setPendingResponse(null);
87
90
  setError(null);
88
91
  setBusy(true);
89
92
  try {
@@ -133,8 +136,8 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
133
136
  description: occurrence.description,
134
137
  descriptionHtml: occurrence.descriptionHtml,
135
138
  guestAddresses: occurrence.attendees.map((a) => a.address),
136
- }, canChange: canChange, canInvite: canInvite, onSent: () => setRequesting(false), onCancel: () => setRequesting(false) }) })), myResponse !== undefined && (_jsxs("div", { className: "flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("span", { className: "text-sm font-medium", children: "Your response" }), _jsx("div", { className: "flex gap-2", children: answers.map(({ response, label }) => {
139
+ }, canChange: canChange, canInvite: canInvite, onSent: () => setRequesting(false), onCancel: () => setRequesting(false) }) })), myResponse !== undefined && canWrite && (_jsxs("div", { className: "flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("span", { className: "text-sm font-medium", children: "Your response" }), _jsx("div", { className: "flex gap-2", children: answers.map(({ response, label }) => {
137
140
  const current = myResponse === response;
138
- return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" "), "aria-pressed": current, disabled: busy, onClick: () => void handleRespond(response), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
139
- }) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Other attendees\u2019 responses may take a few minutes to update." })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-3 px-5 pb-4", children: [!isInvited && (_jsx(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: onModify, children: "Modify" })), (canChange || canInvite) && !requesting && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setRequesting(true), children: canChange ? "Request a change" : "Add guests" })), !occurrence.isRecurringOccurrence ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Delete" })) : !confirmingDelete ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => setConfirmingDelete(true), children: "Delete" })) : (_jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("occurrence"), children: "Delete this event" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Delete series" })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto ml-auto", onClick: onClose, children: "Close" })] })] }));
141
+ return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" "), "aria-pressed": current, disabled: busy, onClick: () => (occurrence.isRecurringOccurrence ? setPendingResponse(response) : void handleRespond(response)), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
142
+ }) })] }), pendingResponse && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsxs("span", { className: "flex-1 min-w-0", children: [pendingResponse === "declined" ? "Declining" : "Your answer", " applies to every event in this series, not just this one", pendingResponse === "declined" ? ", and removes the series from your calendar" : "", "."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setPendingResponse(null), children: "Cancel" }), _jsxs(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: () => void handleRespond(pendingResponse), children: [answers.find((a) => a.response === pendingResponse).label, " the series"] })] })), _jsx("p", { className: "text-xs text-text-muted", children: "Other attendees\u2019 responses may take a few minutes to update." })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-3 px-5 pb-4", children: [!isInvited && canWrite && (_jsx(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: onModify, children: "Modify" })), (canChange || canInvite) && !requesting && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setRequesting(true), children: canChange ? "Request a change" : "Add guests" })), !canWrite ? null : !confirmingDelete ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => setConfirmingDelete(true), children: "Delete" })) : !occurrence.isRecurringOccurrence ? (_jsxs("div", { role: "alert", className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm", children: "Delete this event?" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Yes, delete" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setConfirmingDelete(false), children: "Keep" })] })) : (_jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("occurrence"), children: "Delete this event" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Delete series" })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto ml-auto", onClick: onClose, children: "Close" })] })] }));
140
143
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useMemo, useState } from "react";
6
+ import { useEffect, useMemo, useRef, useState } from "react";
7
7
  import { format } from "date-fns";
8
8
  import { ApiRequestError } from "../../../../lib/util/api.js";
9
9
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
@@ -46,19 +46,25 @@ function wallTimeOfDayMs(instantMs, timezone) {
46
46
  const wall = toEventWallClock(instantMs, timezone, false);
47
47
  return ((wall % MS_PER_DAY) + MS_PER_DAY) % MS_PER_DAY;
48
48
  }
49
+ /** The calendar date (`YYYY-MM-DD`) an instant falls on in the frame a series expands in: the UTC date for an all-day event, `timezone`'s wall clock otherwise. */
50
+ function seriesDateKey(iso, allDay, timezone) {
51
+ return allDay ? allDayDateKey(iso) : new Date(toEventWallClock(new Date(iso).getTime(), timezone, false)).toISOString().slice(0, 10);
52
+ }
53
+ const WEEKDAY_ORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
49
54
  /**
50
55
  * Rewrites an edited occurrence's `startDate`/`endDate` for a save to the entire series. The modal only
51
56
  * ever shows (and edits) *this occurrence's* dates, but the series is stored as its master record starting
52
57
  * at the first occurrence - sending the occurrence's dates as-is would move the whole series' start to
53
- * this occurrence. So only the change in time-of-day (and the new duration) is applied to the master's own
54
- * start; the series keeps its original first date. Unchanged times aren't sent at all.
58
+ * this occurrence. So the change made to the occurrence (its date and its time of day) is applied to the
59
+ * master's own start, and the new duration is kept: moving a Tuesday occurrence to Thursday moves the series
60
+ * by two days, never silently dropping the change. Unchanged times aren't sent at all.
55
61
  *
56
- * Times of day are read on the event's own timezone wall clock (the series expands there), not the
57
- * browser's, and the change is the smallest signed difference modulo 24h - moving 23:00 to 01:00 is two
58
- * hours later (into the next day), never 22 hours earlier. The master's new start is converted back from
59
- * that wall clock, so a master on the other side of a DST change keeps the chosen local time.
62
+ * The change is measured on the event's own timezone wall clock (the series expands there), not the
63
+ * browser's. The master's new start is converted back from that wall clock, so a master on the other side of
64
+ * a DST change keeps the chosen local time. A weekly rule that the edit left untouched is rotated by the
65
+ * same number of days, so its weekdays move with the start (a rule the form already changed is left as it is).
60
66
  */
61
- async function toSeriesFields(occurrence, fields, client) {
67
+ async function toSeriesFields(occurrence, fields, formZone, client) {
62
68
  const { startDate, endDate, ...rest } = fields;
63
69
  if (sameMinute(startDate, occurrence.startDate) && sameMinute(endDate, occurrence.endDate)) {
64
70
  // `saveEventSeries()` treats *any* `timezone`/`allDay` in the update as a possible reinterpretation of
@@ -71,25 +77,32 @@ async function toSeriesFields(occurrence, fields, client) {
71
77
  const timezone = fields.timezone;
72
78
  const newStart = new Date(startDate).getTime();
73
79
  const durationMs = new Date(endDate).getTime() - newStart;
80
+ // The days the date moved: on the event's own wall clock when both are timed, otherwise on the clock the form showed the timed side on.
81
+ const frame = fields.allDay || occurrence.allDay ? formZone : timezone;
82
+ const dayShift = Math.round((Date.parse(seriesDateKey(startDate, !!fields.allDay, frame)) - Date.parse(seriesDateKey(occurrence.startDate, occurrence.allDay, frame))) / MS_PER_DAY);
74
83
  let masterStart;
75
84
  if (fields.allDay) {
76
- // Timed -> all-day keeps the master's own date on its wall clock.
85
+ // The master's own date (on its wall clock when it was timed), moved by the days the occurrence moved.
77
86
  const masterDateKey = master.allDay
78
87
  ? allDayDateKey(master.startDate)
79
88
  : new Date(toEventWallClock(new Date(master.startDate).getTime(), timezone, false)).toISOString().slice(0, 10);
80
- masterStart = new Date(allDayInstant(masterDateKey)).getTime();
89
+ masterStart = new Date(allDayInstant(addDaysToKey(masterDateKey, dayShift))).getTime();
81
90
  }
82
91
  else if (master.allDay) {
83
- // All-day -> timed: the master's own date at the newly chosen wall-clock time.
84
- const masterDateWall = new Date(allDayInstant(allDayDateKey(master.startDate))).getTime();
92
+ // All-day -> timed: the master's own date (moved by the days the occurrence moved) at the newly chosen wall-clock time.
93
+ const masterDateWall = new Date(allDayInstant(addDaysToKey(allDayDateKey(master.startDate), dayShift))).getTime();
85
94
  masterStart = fromEventWallClock(masterDateWall + wallTimeOfDayMs(newStart, timezone), timezone, false);
86
95
  }
87
96
  else {
88
- const rawDelta = wallTimeOfDayMs(newStart, timezone) - wallTimeOfDayMs(new Date(occurrence.startDate).getTime(), timezone);
89
- const delta = ((((rawDelta + MS_PER_DAY / 2) % MS_PER_DAY) + MS_PER_DAY) % MS_PER_DAY) - MS_PER_DAY / 2;
97
+ const delta = toEventWallClock(newStart, timezone, false) - toEventWallClock(new Date(occurrence.startDate).getTime(), timezone, false);
90
98
  const masterWall = toEventWallClock(new Date(master.startDate).getTime(), timezone, false);
91
99
  masterStart = fromEventWallClock(masterWall + delta, timezone, false);
92
100
  }
101
+ const rule = rest.recurrenceRule;
102
+ const original = occurrence.recurrenceRule?.byDay;
103
+ if (rule?.freq === "weekly" && rule.byDay && original && dayShift % 7 !== 0 && rule.byDay.join() === original.join()) {
104
+ rest.recurrenceRule = { ...rule, byDay: rule.byDay.map((day) => WEEKDAY_ORDER[(WEEKDAY_ORDER.indexOf(day) + (dayShift % 7) + 7) % 7]) };
105
+ }
93
106
  return {
94
107
  ...rest,
95
108
  startDate: new Date(masterStart).toISOString(),
@@ -150,6 +163,8 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
150
163
  targetFolderUid: folderUid,
151
164
  };
152
165
  });
166
+ // What an existing event's form held when it opened, to tell whether anything was changed since.
167
+ const [pristine] = useState(() => JSON.stringify(values));
153
168
  const { attendees, location, videoEnabled } = values;
154
169
  function update(patch) {
155
170
  setValues((prev) => ({ ...prev, ...patch }));
@@ -172,6 +187,12 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
172
187
  // finish the video-conferencing step after it: the modal then stays open on that error, and the retry
173
188
  // must update *that* record (its uid and its new version), not re-create it or reuse the stale prop.
174
189
  const [savedEvent, setSavedEvent] = useState(null);
190
+ // Whether that stored record is a series saved with `toSeriesFields()`: its master's dates are not the form's (the occurrence's), so a retry
191
+ // must not send them - the first save already applied any change of time.
192
+ const savedAsSeries = useRef(false);
193
+ // A meeting minted for this event whose link onto the event could not be saved. A retry reuses it rather than minting a second one,
194
+ // which would send the invitees a second set of join links and leave this one orphaned.
195
+ const mintedMeeting = useRef(null);
175
196
  const [editScope, setEditScope] = useState("occurrence");
176
197
  const [error, setError] = useState(null);
177
198
  const [saving, setSaving] = useState(false);
@@ -201,7 +222,9 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
201
222
  }, [occurrence?.videoMeetingUid, client]);
202
223
  // "This event only" detaches a standalone, non-repeating copy - the series' rule doesn't apply to it.
203
224
  const editingSingleOccurrence = !!occurrence?.isRecurringOccurrence && editScope === "occurrence";
204
- dirtyRef.current = !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
225
+ dirtyRef.current = occurrence
226
+ ? JSON.stringify(values) !== pristine
227
+ : !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
205
228
  function updateAttendee(index, patch) {
206
229
  setValues((prev) => ({ ...prev, attendees: prev.attendees.map((a, i) => (i === index ? { ...a, ...patch } : a)) }));
207
230
  }
@@ -217,6 +240,11 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
217
240
  ],
218
241
  }));
219
242
  }
243
+ // The weekday the form's start falls on, in the frame the series expands in (`undefined` while the start is not a valid date).
244
+ function startWeekdayOf(v) {
245
+ const ms = v.allDay ? Number.NaN : wallStringToMs(v.start, v.formZone, deviceZone);
246
+ return startWeekdayCode(v.allDay ? v.start : Number.isNaN(ms) ? "" : new Date(ms).toISOString(), v.allDay, v.timezone);
247
+ }
220
248
  // Moving the start moves the end with it, so the event keeps its length (a Google Calendar habit, and the way to
221
249
  // reschedule without touching two fields).
222
250
  function setStart(next) {
@@ -231,7 +259,13 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
231
259
  ? `${new Date(endMs + shift).toISOString().slice(0, 10)}T00:00`
232
260
  : msToWallString(endMs + shift, prev.formZone, deviceZone);
233
261
  }
234
- return { ...prev, start: next, end: nextEnd };
262
+ // A weekly rule on just the start's weekday ("Weekly on Wednesday") follows the start to its new weekday - left alone it
263
+ // would not include the event's own first day, and the series would begin on the next of the old weekday instead.
264
+ const oldDay = startWeekdayOf(prev);
265
+ const nextDay = startWeekdayOf({ ...prev, start: next });
266
+ const rule = prev.recurrenceRule;
267
+ const followsStart = oldDay && nextDay && rule?.freq === "weekly" && rule.byDay?.length === 1 && rule.byDay[0] === oldDay;
268
+ return { ...prev, start: next, end: nextEnd, recurrenceRule: followsStart ? { ...rule, byDay: [nextDay] } : rule };
235
269
  });
236
270
  }
237
271
  // The recurrence end date is stored in the frame the series expands in (UTC for all-day, local otherwise),
@@ -331,23 +365,28 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
331
365
  setVideoError("Add at least one attendee other than yourself to add video conferencing.");
332
366
  return { event: saved, failed: true };
333
367
  }
334
- const result = await createVideoMeeting({
335
- mailboxUid: saved.mailboxUid,
336
- title: saved.title.slice(0, MAX_MEETING_TITLE_LENGTH),
337
- visibility: "private",
338
- calendarEventUid: saved.uid,
339
- startTime: saved.startDate,
340
- endTime: saved.endDate,
341
- invitees,
342
- }, client);
368
+ if (!mintedMeeting.current) {
369
+ const result = await createVideoMeeting({
370
+ mailboxUid: saved.mailboxUid,
371
+ title: saved.title.slice(0, MAX_MEETING_TITLE_LENGTH),
372
+ visibility: "private",
373
+ calendarEventUid: saved.uid,
374
+ startTime: saved.startDate,
375
+ endTime: saved.endDate,
376
+ invitees,
377
+ }, client);
378
+ mintedMeeting.current = { uid: result.meeting.uid, organizerJoinUrl: result.organizerJoinUrl ?? null };
379
+ }
380
+ const meeting = mintedMeeting.current;
343
381
  const patched = await updateCalendarEvent({
344
382
  uid: saved.uid,
345
383
  version: saved.version,
346
384
  location: VIDEO_LOCATION_PLACEHOLDER,
347
- videoMeetingUid: result.meeting.uid,
385
+ videoMeetingUid: meeting.uid,
348
386
  }, client);
349
- setVideoMeetingUid(result.meeting.uid);
350
- setOrganizerJoinUrl(result.organizerJoinUrl ?? null);
387
+ mintedMeeting.current = null;
388
+ setVideoMeetingUid(meeting.uid);
389
+ setOrganizerJoinUrl(meeting.organizerJoinUrl);
351
390
  update({ location: VIDEO_LOCATION_PLACEHOLDER });
352
391
  return { event: patched, failed: false };
353
392
  }
@@ -455,8 +494,17 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
455
494
  // A retry after the event was stored but its video meeting couldn't be (see `savedEvent`):
456
495
  // update exactly the record the first attempt left behind, whichever branch below created it.
457
496
  // A series' own exception/detached-occurrence re-pointing already ran on that attempt and has
458
- // nothing left to shift - the dates being sent again are the ones it moved everything to.
459
- saved = await updateCalendarEvent({ uid: savedEvent.uid, version: savedEvent.version, ...fields }, client);
497
+ // nothing left to shift. For a series the form's dates are those of the edited occurrence, not
498
+ // the master's: sending them would move the whole series to that occurrence, so they (and the
499
+ // zone and all-day flag they go with) stay as the first attempt left them.
500
+ const retryFields = { ...fields };
501
+ if (savedAsSeries.current) {
502
+ delete retryFields.startDate;
503
+ delete retryFields.endDate;
504
+ delete retryFields.timezone;
505
+ delete retryFields.allDay;
506
+ }
507
+ saved = await updateCalendarEvent({ uid: savedEvent.uid, version: savedEvent.version, ...retryFields }, client);
460
508
  }
461
509
  else if (!occurrence) {
462
510
  // The organizer is only ever set on create - an edit keeps the event's own organizer.
@@ -471,8 +519,9 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
471
519
  saved = await detachOccurrence(occurrence, fields, client);
472
520
  }
473
521
  else if (occurrence.isRecurringOccurrence) {
474
- const series = await saveEventSeries(occurrence, (await toSeriesFields(occurrence, fields, client)), client);
522
+ const series = await saveEventSeries(occurrence, (await toSeriesFields(occurrence, fields, values.formZone, client)), client);
475
523
  saved = series;
524
+ savedAsSeries.current = true;
476
525
  detachedSyncFailed = !!series.detachedOccurrenceSyncFailed;
477
526
  }
478
527
  else {
@@ -507,7 +556,6 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
507
556
  // stays disabled either way, and the two are told apart below purely from `organizerJoinUrl` itself.
508
557
  const joinUrl = joinMeetingUrl(organizerJoinUrl);
509
558
  const timeZones = useMemo(() => timeZoneOptions(values.formZone, values.timezone, deviceZone), [values.formZone, values.timezone, deviceZone]);
510
- const startMs = values.allDay ? Number.NaN : wallStringToMs(values.start, values.formZone, deviceZone);
511
559
  const calendarFolderUid = occurrence ? occurrence.folderUid : values.targetFolderUid;
512
560
  const controller = {
513
561
  occurrence,
@@ -520,7 +568,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
520
568
  timeZones,
521
569
  organizerAddress: effectiveOrganizerAddress,
522
570
  deviceZone,
523
- startWeekday: startWeekdayCode(values.allDay ? values.start : Number.isNaN(startMs) ? "" : new Date(startMs).toISOString(), values.allDay, values.timezone),
571
+ startWeekday: startWeekdayOf(values),
524
572
  editingSingleOccurrence,
525
573
  updateAttendee,
526
574
  removeAttendee,
@@ -38,11 +38,13 @@ export declare function ReminderRow({ value, onChange }: {
38
38
  * "Does not repeat" and the plain repeats as a menu, with "Custom…" opening `RecurrenceEditor`'s full set (an interval, weekdays, an end).
39
39
  * Choosing one of the plain repeats keeps the rule's skipped dates; the menu shows "Custom…" for any rule it has no plain entry for.
40
40
  */
41
- export declare function RecurrenceSelect({ value, onChange, allDay, startWeekday, }: {
41
+ export declare function RecurrenceSelect({ value, onChange, allDay, startWeekday, startDateKey, }: {
42
42
  value: RecurrenceRule | null;
43
43
  onChange: (rule: RecurrenceRule | null) => void;
44
44
  allDay: boolean;
45
45
  startWeekday: WeekdayCode | undefined;
46
+ /** The event's start date (`yyyy-MM-dd`), for a custom rule's default end date. */
47
+ startDateKey?: string;
46
48
  }): React.JSX.Element;
47
49
  /** [start date] [start time] to [end time] [end date], then the "Time zone" link (and its menu), "All day" and how the event repeats. */
48
50
  export declare function DateTimeControls({ c }: {
@@ -124,7 +124,7 @@ function presetOf(rule, startWeekday) {
124
124
  * "Does not repeat" and the plain repeats as a menu, with "Custom…" opening `RecurrenceEditor`'s full set (an interval, weekdays, an end).
125
125
  * Choosing one of the plain repeats keeps the rule's skipped dates; the menu shows "Custom…" for any rule it has no plain entry for.
126
126
  */
127
- export function RecurrenceSelect({ value, onChange, allDay, startWeekday, }) {
127
+ export function RecurrenceSelect({ value, onChange, allDay, startWeekday, startDateKey, }) {
128
128
  // Picking "Custom…" for a rule that is also a plain repeat still opens the editor.
129
129
  const [customRequested, setCustomRequested] = useState(false);
130
130
  const selected = customRequested && value ? "custom" : presetOf(value, startWeekday);
@@ -140,7 +140,7 @@ export function RecurrenceSelect({ value, onChange, allDay, startWeekday, }) {
140
140
  onChange({ ...presetRule(key, startWeekday), exceptions: value?.exceptions ?? [] });
141
141
  }
142
142
  }
143
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("select", { id: "event-recurrence", "aria-label": "Recurrence", className: SELECT_CLASS, value: selected, onChange: (e) => choose(e.target.value), children: [_jsx("option", { value: "none", children: "Does not repeat" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "weekly", children: startWeekday ? `Weekly on ${WEEKDAY_NAME[startWeekday]}` : "Weekly" }), _jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Annually" }), _jsx("option", { value: "weekdays", children: "Every weekday (Monday to Friday)" }), _jsx("option", { value: "custom", children: "Custom\u2026" })] }), selected === "custom" && _jsx(RecurrenceEditor, { value: value, onChange: onChange, allDay: allDay, startWeekday: startWeekday, hideToggle: true })] }));
143
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("select", { id: "event-recurrence", "aria-label": "Recurrence", className: SELECT_CLASS, value: selected, onChange: (e) => choose(e.target.value), children: [_jsx("option", { value: "none", children: "Does not repeat" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "weekly", children: startWeekday ? `Weekly on ${WEEKDAY_NAME[startWeekday]}` : "Weekly" }), _jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Annually" }), _jsx("option", { value: "weekdays", children: "Every weekday (Monday to Friday)" }), _jsx("option", { value: "custom", children: "Custom\u2026" })] }), selected === "custom" && _jsx(RecurrenceEditor, { value: value, onChange: onChange, allDay: allDay, startWeekday: startWeekday, startDateKey: startDateKey, hideToggle: true })] }));
144
144
  }
145
145
  const START_TIME_OPTIONS = dayTimes().map((time) => ({ value: time, label: formatTimeOfDay(time) }));
146
146
  /** [start date] [start time] to [end time] [end date], then the "Time zone" link (and its menu), "All day" and how the event repeats. */
@@ -149,7 +149,7 @@ export function DateTimeControls({ c }) {
149
149
  const [zoneOpen, setZoneOpen] = useState(false);
150
150
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsx("input", { type: "date", "aria-label": "Event start date", className: `${SELECT_CLASS} min-w-0`, value: values.start.slice(0, 10), onChange: (e) => c.setStart(`${e.target.value}${values.start.slice(10)}`) }), !values.allDay && (_jsx(TimeComboBox, { "aria-label": "Event start time", className: `${SELECT_CLASS} w-28`, display: formatTimeOfDay(values.start.slice(11, 16)), current: values.start.slice(11, 16), options: START_TIME_OPTIONS,
151
151
  // Moving the start moves the end with it, so the event keeps its length: whatever the start is set to.
152
- onSelect: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`), onType: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`) })), _jsx("span", { className: "text-text-muted", children: "to" }), !values.allDay && (_jsx(TimeComboBox, { "aria-label": "Event end time", className: `${SELECT_CLASS} w-28`, display: formatTimeOfDay(values.end.slice(11, 16)), current: values.end, options: endTimeOptions(values.start).map((option) => ({ value: option.value, label: option.label, hint: option.hint })), onSelect: c.setEnd, onType: (time) => c.setEnd(`${values.end.slice(0, 11)}${time}`) })), _jsx("input", { type: "date", "aria-label": "Event end date", className: `${SELECT_CLASS} min-w-0`, value: values.end.slice(0, 10), onChange: (e) => c.setEnd(`${e.target.value}${values.end.slice(10)}`) })] }), _jsx("button", { type: "button", "aria-expanded": zoneOpen, className: "self-start text-sm text-primary-dark hover:underline", onClick: () => setZoneOpen((open) => !open), children: "Time zone" }), zoneOpen && (_jsx(TimeZonePicker, { "aria-label": "Event time zone", className: `${SELECT_CLASS} max-w-full`, value: values.formZone, zones: c.timeZones, at: zoneDate(values.start), onChange: c.onTimeZoneChange })), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: values.allDay, onChange: (e) => c.onAllDayChange(e.target.checked) }), "All day"] }), !c.editingSingleOccurrence && (_jsx(RecurrenceSelect, { value: values.recurrenceRule, onChange: (rule) => c.update({ recurrenceRule: rule }), allDay: values.allDay, startWeekday: c.startWeekday }))] }));
152
+ onSelect: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`), onType: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`) })), _jsx("span", { className: "text-text-muted", children: "to" }), !values.allDay && (_jsx(TimeComboBox, { "aria-label": "Event end time", className: `${SELECT_CLASS} w-28`, display: formatTimeOfDay(values.end.slice(11, 16)), current: values.end, options: endTimeOptions(values.start).map((option) => ({ value: option.value, label: option.label, hint: option.hint })), onSelect: c.setEnd, onType: (time) => c.setEnd(`${values.end.slice(0, 11)}${time}`) })), _jsx("input", { type: "date", "aria-label": "Event end date", className: `${SELECT_CLASS} min-w-0`, value: values.end.slice(0, 10), onChange: (e) => c.setEnd(`${e.target.value}${values.end.slice(10)}`) })] }), _jsx("button", { type: "button", "aria-expanded": zoneOpen, className: "self-start text-sm text-primary-dark hover:underline", onClick: () => setZoneOpen((open) => !open), children: "Time zone" }), zoneOpen && (_jsx(TimeZonePicker, { "aria-label": "Event time zone", className: `${SELECT_CLASS} max-w-full`, value: values.formZone, zones: c.timeZones, at: zoneDate(values.start), onChange: c.onTimeZoneChange })), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: values.allDay, onChange: (e) => c.onAllDayChange(e.target.checked) }), "All day"] }), !c.editingSingleOccurrence && (_jsx(RecurrenceSelect, { value: values.recurrenceRule, onChange: (rule) => c.update({ recurrenceRule: rule }), allDay: values.allDay, startWeekday: c.startWeekday, startDateKey: values.start.slice(0, 10) }))] }));
153
153
  }
154
154
  /** The calendar this event is in: its colour and name, a choice between calendars (and mailboxes) when an event is being created. */
155
155
  export function CalendarField({ c }) {
@@ -12,6 +12,6 @@ export interface EventMatchNavProps {
12
12
  * The Previous / Next match buttons beside an event's card while a search is running: the card moves on to the neighbouring result. They sit
13
13
  * inside the dialog (so Tab reaches them and the dialog's focus trap holds) but are positioned against the window - either side of the
14
14
  * centered card on a desktop, the bottom corners on a phone, where the card fills the width. The left and right arrow keys do the same unless
15
- * a field is taking them.
15
+ * a field or a widget (a select, a menu, a slider, ...) is taking them.
16
16
  */
17
17
  export default function EventMatchNav({ onPrevious, onNext, canPrevious, canNext, noun }: EventMatchNavProps): React.JSX.Element;
@@ -6,16 +6,26 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
6
6
  import { useEffect } from "react";
7
7
  import { HiOutlineChevronLeft, HiOutlineChevronRight } from "react-icons/hi2";
8
8
  import { isTextEntry } from "../../keyboard/targets.js";
9
+ /** Widgets that take the arrow keys themselves (a menu's items, a radio group, a slider, a list's options, a tab list, ...) and an open popup's trigger. */
10
+ const ARROW_WIDGET_SELECTOR = [
11
+ ...["menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "radiogroup", "radio", "listbox", "option"].map((role) => `[role="${role}"]`),
12
+ ...["slider", "spinbutton", "tablist", "tab", "tree", "grid", "combobox"].map((role) => `[role="${role}"]`),
13
+ '[aria-haspopup][aria-expanded="true"]',
14
+ ].join(",");
15
+ /** Whether the arrow keys pressed at `target` belong to what is there: a field or select, or a widget with arrows of its own. */
16
+ function takesArrowKeys(target) {
17
+ return isTextEntry(target) || (target instanceof Element && target.closest(ARROW_WIDGET_SELECTOR) !== null);
18
+ }
9
19
  /**
10
20
  * The Previous / Next match buttons beside an event's card while a search is running: the card moves on to the neighbouring result. They sit
11
21
  * inside the dialog (so Tab reaches them and the dialog's focus trap holds) but are positioned against the window - either side of the
12
22
  * centered card on a desktop, the bottom corners on a phone, where the card fills the width. The left and right arrow keys do the same unless
13
- * a field is taking them.
23
+ * a field or a widget (a select, a menu, a slider, ...) is taking them.
14
24
  */
15
25
  export default function EventMatchNav({ onPrevious, onNext, canPrevious = true, canNext = true, noun = "match" }) {
16
26
  useEffect(() => {
17
27
  function handleKeyDown(event) {
18
- if ((event.key !== "ArrowLeft" && event.key !== "ArrowRight") || event.defaultPrevented || isTextEntry(event.target)) {
28
+ if ((event.key !== "ArrowLeft" && event.key !== "ArrowRight") || event.defaultPrevented || takesArrowKeys(event.target)) {
19
29
  return;
20
30
  }
21
31
  if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
@@ -7,6 +7,7 @@ import { useRef, useState } from "react";
7
7
  import Modal from "../../../../lib/components/overlays/Modal.js";
8
8
  import Button from "../../../../lib/components/buttons/Button.js";
9
9
  import useIsMobile from "../../../../lib/util/useIsMobile.js";
10
+ import { useMailboxUpdateAccess } from "../../mail/useMailboxUpdateAccess.js";
10
11
  import EventDetails from "./EventDetails.js";
11
12
  import EventEditor from "./EventEditor.js";
12
13
  import EventMatchNav from "./EventMatchNav.js";
@@ -43,6 +44,10 @@ export default function EventModal({ open, onClose, mailboxUid, folderUid, calen
43
44
  const [syncWarning, setSyncWarning] = useState(false);
44
45
  // Whether the form holds anything a click on the backdrop would throw away (kept up to date by the editor).
45
46
  const dirtyRef = useRef(false);
47
+ // Escape or the close button on a form that holds something asks before throwing it away.
48
+ const [confirmingDiscard, setConfirmingDiscard] = useState(false);
49
+ // An event on a calendar shared with the reader view-only cannot be changed, deleted or answered.
50
+ const canWrite = useMailboxUpdateAccess(mailboxOptions?.find((option) => option.mailbox.uid === occurrence?.mailboxUid)?.mailbox);
46
51
  if (!open) {
47
52
  return null;
48
53
  }
@@ -65,7 +70,7 @@ export default function EventModal({ open, onClose, mailboxUid, folderUid, calen
65
70
  const width = !isForm ? DETAILS_WIDTH : layout === "expanded" ? (tab === "event" || occurrence ? FORM_WIDTH : TASK_FORM_WIDTH) : QUICK_WIDTH;
66
71
  // Leaving the form: an existing event goes back to its details, a new one is done.
67
72
  const leaveForm = occurrence ? () => setEditing(false) : onClose;
68
- return (_jsxs(EventShell, { variant: variant, label: !occurrence ? "New event" : editing ? "Edit event" : "Event details", focusKey: isForm ? layout : "details", anchor: anchor, width: width, onClose: isForm ? leaveForm : onClose, onBackdropPress: isForm ? () => !dirtyRef.current && leaveForm() : undefined, children: [isForm ? (_jsx(EventEditor, { layout: layout, onExpand: () => setExpanded(true), onCancel: leaveForm, onSaved: onSaved, onSyncWarning: () => setSyncWarning(true), dirtyRef: dirtyRef, mailboxUid: mailboxUid, folderUid: folderUid, calendars: calendars, mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: organizerAddress, occurrence: occurrence, initialStart: initialStart, initialEnd: initialEnd, initialAllDay: initialAllDay, quickCreate: occurrence
73
+ return (_jsxs(EventShell, { variant: variant, label: !occurrence ? "New event" : editing ? "Edit event" : "Event details", focusKey: isForm ? layout : "details", anchor: anchor, width: width, onClose: isForm ? () => (dirtyRef.current ? setConfirmingDiscard(true) : leaveForm()) : onClose, onBackdropPress: isForm ? () => !dirtyRef.current && leaveForm() : undefined, children: [isForm ? (_jsx(EventEditor, { layout: layout, onExpand: () => setExpanded(true), onCancel: leaveForm, onSaved: onSaved, onSyncWarning: () => setSyncWarning(true), dirtyRef: dirtyRef, mailboxUid: mailboxUid, folderUid: folderUid, calendars: calendars, mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: organizerAddress, occurrence: occurrence, initialStart: initialStart, initialEnd: initialEnd, initialAllDay: initialAllDay, quickCreate: occurrence
69
74
  ? undefined
70
75
  : {
71
76
  tab,
@@ -74,5 +79,8 @@ export default function EventModal({ open, onClose, mailboxUid, folderUid, calen
74
79
  setExpanded(false);
75
80
  },
76
81
  bookingHref,
77
- } })) : (_jsx(SlideIn, { from: matchNav?.slide?.occurrenceKey === occurrence.occurrenceKey ? matchNav.slide.from : undefined, children: _jsx(EventDetails, { occurrence: occurrence, isInvited: isInvited, myResponse: canRespond ? myAttendee.responseStatus : undefined, isOwnAddress: isOwnAddress, calendarName: calendars?.find((cal) => cal.uid === occurrence.folderUid)?.name, calendarColor: folderColors?.[occurrence.folderUid], onClose: onClose, onModify: () => setEditing(true), onSaved: onSaved, onDeleted: onDeleted }) }, occurrence.occurrenceKey)), !isForm && matchNav && _jsx(EventMatchNav, { onPrevious: matchNav.onPrevious, onNext: matchNav.onNext, canPrevious: matchNav.canPrevious, canNext: matchNav.canNext, noun: matchNav.noun })] }));
82
+ } })) : (_jsx(SlideIn, { from: matchNav?.slide?.occurrenceKey === occurrence.occurrenceKey ? matchNav.slide.from : undefined, children: _jsx(EventDetails, { occurrence: occurrence, isInvited: isInvited, myResponse: canRespond ? myAttendee.responseStatus : undefined, isOwnAddress: isOwnAddress, calendarName: calendars?.find((cal) => cal.uid === occurrence.folderUid)?.name, calendarColor: folderColors?.[occurrence.folderUid], canWrite: canWrite, onClose: onClose, onModify: () => setEditing(true), onSaved: onSaved, onDeleted: onDeleted }) }, occurrence.occurrenceKey)), !isForm && matchNav && _jsx(EventMatchNav, { onPrevious: matchNav.onPrevious, onNext: matchNav.onNext, canPrevious: matchNav.canPrevious, canNext: matchNav.canNext, noun: matchNav.noun }), _jsxs(Modal, { open: confirmingDiscard, onClose: () => setConfirmingDiscard(false), title: "Discard changes?", children: [_jsx("p", { className: "text-sm mb-5", children: "What you entered in this event has not been saved." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDiscard(false), children: "Keep editing" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => {
83
+ setConfirmingDiscard(false);
84
+ leaveForm();
85
+ }, children: "Discard" })] })] })] }));
78
86
  }
@@ -69,7 +69,7 @@ export default function FindATime({ c }) {
69
69
  }
70
70
  }, [startDay]);
71
71
  // You, then each guest once (case-insensitively), at most as many as one look-up may ask about.
72
- const rows = useMemo(() => {
72
+ const { rows, skipped } = useMemo(() => {
73
73
  const seen = new Set([organizerAddress.toLowerCase()]);
74
74
  const guests = [];
75
75
  for (const attendee of values.attendees) {
@@ -79,7 +79,8 @@ export default function FindATime({ c }) {
79
79
  guests.push({ address: attendee.address, name: attendee.displayName || attendee.address, you: false });
80
80
  }
81
81
  }
82
- return [{ address: organizerAddress, name: "You", you: true }, ...guests].slice(0, FREE_BUSY_MAX_ADDRESSES);
82
+ const everyone = [{ address: organizerAddress, name: "You", you: true }, ...guests];
83
+ return { rows: everyone.slice(0, FREE_BUSY_MAX_ADDRESSES), skipped: Math.max(0, everyone.length - FREE_BUSY_MAX_ADDRESSES) };
83
84
  }, [values.attendees, organizerAddress]);
84
85
  const guestCount = rows.length - 1;
85
86
  const addresses = rows.map((row) => row.address);
@@ -168,7 +169,7 @@ export default function FindATime({ c }) {
168
169
  .map((block) => `${block.tentative ? "Tentatively busy" : "Busy"} ${clockLabel(block.startMs, formZone, deviceZone)} to ${clockLabel(block.endMs, formZone, deviceZone)}`)
169
170
  .join(", ");
170
171
  }
171
- return (_jsxs("div", { className: "flex flex-col gap-3 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: () => setDayKey((key) => addDaysToKey(key, -1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u2039" }), _jsx("span", { className: "text-sm font-medium min-w-[9rem] text-center", children: dayLabel }), _jsx("button", { type: "button", "aria-label": "Next day", onClick: () => setDayKey((key) => addDaysToKey(key, 1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u203A" }), _jsx("span", { className: "text-xs text-text-muted", children: describeTimeZone(formZone, zoneDate(dayKey)) })] }), lookUp.status === "error" && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-3 text-sm text-danger", children: [_jsx("span", { children: lookUp.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRetry((count) => count + 1), children: "Try again" })] })), _jsx("div", { ref: scroller, role: "group", "aria-label": "Guests' availability", "aria-busy": lookUp.status === "loading", className: "overflow-x-auto border border-border rounded-md", children: _jsxs("div", { className: "relative", style: { width: NAME_WIDTH + TIMELINE_WIDTH }, children: [_jsxs("div", { className: "flex bg-surface border-b border-border", style: { height: HEADER_HEIGHT }, children: [_jsx("div", { className: "sticky left-0 z-20 shrink-0 bg-surface border-r border-border", style: { width: NAME_WIDTH } }), HOURS.map((hour) => (_jsx("button", { type: "button", "aria-label": `Start at ${hourLabel(hour)}`, title: `Start at ${hourLabel(hour)}`, onClick: () => startAt(hour * 60), className: "shrink-0 text-[10px] text-text-muted hover:bg-surface-alt hover:text-text text-left pl-1", style: { width: HOUR_WIDTH }, children: hourLabel(hour) }, hour)))] }), rows.map((row) => {
172
+ return (_jsxs("div", { className: "flex flex-col gap-3 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: () => setDayKey((key) => addDaysToKey(key, -1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u2039" }), _jsx("span", { className: "text-sm font-medium min-w-[9rem] text-center", children: dayLabel }), _jsx("button", { type: "button", "aria-label": "Next day", onClick: () => setDayKey((key) => addDaysToKey(key, 1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u203A" }), _jsx("span", { className: "text-xs text-text-muted", children: describeTimeZone(formZone, zoneDate(dayKey)) })] }), skipped > 0 && (_jsxs("p", { className: "text-xs text-text-muted", children: [skipped, " ", skipped === 1 ? "guest" : "guests", " beyond the first ", FREE_BUSY_MAX_ADDRESSES - 1, " ", skipped === 1 ? "isn’t" : "aren’t", " checked, and ", skipped === 1 ? "is" : "are", " not counted in who is free."] })), lookUp.status === "error" && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-3 text-sm text-danger", children: [_jsx("span", { children: lookUp.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRetry((count) => count + 1), children: "Try again" })] })), _jsx("div", { ref: scroller, role: "group", "aria-label": "Guests' availability", "aria-busy": lookUp.status === "loading", className: "overflow-x-auto border border-border rounded-md", children: _jsxs("div", { className: "relative", style: { width: NAME_WIDTH + TIMELINE_WIDTH }, children: [_jsxs("div", { className: "flex bg-surface border-b border-border", style: { height: HEADER_HEIGHT }, children: [_jsx("div", { className: "sticky left-0 z-20 shrink-0 bg-surface border-r border-border", style: { width: NAME_WIDTH } }), HOURS.map((hour) => (_jsx("button", { type: "button", "aria-label": `Start at ${hourLabel(hour)}`, title: `Start at ${hourLabel(hour)}`, onClick: () => startAt(hour * 60), className: "shrink-0 text-[10px] text-text-muted hover:bg-surface-alt hover:text-text text-left pl-1", style: { width: HOUR_WIDTH }, children: hourLabel(hour) }, hour)))] }), rows.map((row) => {
172
173
  const person = byAddress.get(row.address.toLowerCase());
173
174
  const state = person && timeIsValid && covered ? availabilityDuring(person, eventStart, eventEnd) : undefined;
174
175
  const hidden = person && person.status !== "available" ? person : undefined;
@@ -1,7 +1,10 @@
1
1
  import React from "react";
2
2
  import { CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
3
- /** The most days one event is listed on, so an event that runs for years does not become thousands of rows. */
3
+ /** The most days one event is listed on (counted from today), so an event that runs for years does not become thousands of rows. */
4
4
  export declare const LIST_MAX_DAYS_PER_EVENT = 366;
5
+ /** The days (that have events) rendered at first, and how many more each time the end of the list is reached. */
6
+ export declare const LIST_INITIAL_DAYS = 60;
7
+ export declare const LIST_DAYS_PER_PAGE = 60;
5
8
  /** A step through the list's months: `nonce` makes a second press in the same direction a new step. */
6
9
  export interface ListStep {
7
10
  direction: 1 | -1;
@@ -24,7 +27,10 @@ export interface ListViewProps {
24
27
  * nothing at all says so). A multi-day event is listed on every day it covers, as the month grid shows it. Each row is a full-width button that
25
28
  * opens the event's card, with the time, title, place (or organizer) and its calendar's colour.
26
29
  *
27
- * Every day is rendered (an event is a cheap row, and a series is bounded to 500 occurrences - see `eventSearch.ts`); the days are grouped once per
28
- * change of `occurrences`, not per render. The list scrolls itself to `focusDate` and, on a `step`, to the neighbouring month.
30
+ * The days are grouped once per change of `occurrences` (or of the day), not per render, but only the first `LIST_INITIAL_DAYS` of them are rendered:
31
+ * more follow as the end of the list is scrolled to (a daily series alone is thousands of rows). A jump, a step or a search match that lies beyond
32
+ * the rendered days renders up to it first. The list scrolls itself to `focusDate` and, on a `step`, to the neighbouring month.
29
33
  */
30
- export default function ListView({ occurrences, folderColors, onSelectEvent, focusDate, jumpNonce, step }: ListViewProps): React.JSX.Element;
34
+ declare function ListView({ occurrences, folderColors, onSelectEvent, focusDate, jumpNonce, step }: ListViewProps): React.JSX.Element;
35
+ declare const _default: React.MemoExoticComponent<typeof ListView>;
36
+ export default _default;