@rapidmx/web-client 0.30.0 → 0.31.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 (400) 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 +1 -1
  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/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -4,14 +4,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { createEscrowScope, listEscrowScopes } from "../../../../../lib/admin/escrowScopesApi.js";
9
8
  import { generateEscrowKeyPair } from "../../../../../lib/crypto/escrowKeys.js";
10
9
  import { toDatetimeLocal } from "../../../../../lib/util/dateInput.js";
11
- import Alert from "../../../../../lib/components/feedback/Alert.js";
10
+ import Alert from "../ActionAlert.js";
12
11
  import Button from "../../../../../lib/components/buttons/Button.js";
13
12
  import FormField from "../../../../../lib/components/forms/FormField.js";
14
- import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, selfAsHolderError, } from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
13
+ import Modal from "../../../../../lib/components/overlays/Modal.js";
14
+ import { actionErrorMessage } from "../elevation.js";
15
+ import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, keyValidityError, selfAsHolderError, } from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
15
16
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
16
17
  /** How long a download's object URL is kept, since some browsers are still reading it after the click returns. */
17
18
  export const DOWNLOAD_URL_LIFETIME_MS = 60000;
@@ -43,6 +44,8 @@ export default function EscrowSetupStep({ adminUid }) {
43
44
  const [generated, setGenerated] = useState(null);
44
45
  const [downloadedPrivateKey, setDownloadedPrivateKey] = useState(false);
45
46
  const [savedPrivateKey, setSavedPrivateKey] = useState(false);
47
+ // The private key leaves the browser as a plain file, so it is only offered after the administrator has read what that means.
48
+ const [confirmingDownload, setConfirmingDownload] = useState(false);
46
49
  const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
47
50
  const [error, setError] = useState(null);
48
51
  const [busy, setBusy] = useState(false);
@@ -57,6 +60,7 @@ export default function EscrowSetupStep({ adminUid }) {
57
60
  setGenerated(null);
58
61
  setDownloadedPrivateKey(false);
59
62
  setSavedPrivateKey(false);
63
+ setConfirmingDownload(false);
60
64
  setKeyAndHolders(emptyEscrowScopeKeyAndHoldersValue());
61
65
  }
62
66
  async function generate() {
@@ -106,6 +110,11 @@ export default function EscrowSetupStep({ adminUid }) {
106
110
  setError("The public key, its type, and its fingerprint are all required.");
107
111
  return;
108
112
  }
113
+ const validityError = generated ? null : keyValidityError(keyAndHolders);
114
+ if (validityError) {
115
+ setError(validityError);
116
+ return;
117
+ }
109
118
  if (keyAndHolders.holderUserUids.length === 0) {
110
119
  setError("At least one holder is required.");
111
120
  return;
@@ -133,15 +142,16 @@ export default function EscrowSetupStep({ adminUid }) {
133
142
  setName("Escrow");
134
143
  }
135
144
  catch (err) {
136
- setError(err instanceof ApiRequestError ? err.message : "Could not create the escrow scope.");
145
+ setError(actionErrorMessage(err, "Could not create the escrow scope."));
137
146
  }
138
147
  finally {
139
148
  setBusy(false);
140
149
  }
141
150
  }
142
151
  const scopeForm = (_jsxs("form", { onSubmit: create, className: "flex flex-col gap-5", children: [_jsx(EscrowScopeKeyAndHoldersFields, { value: keyAndHolders, onChange: setKeyAndHolders, disabled: mode === "generate" && busy, keyReadOnly: mode === "generate" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: busy, disabled: busy, className: "!w-auto", children: "Create escrow scope" }) })] }));
143
- return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
144
- downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
145
- setDownloadedPrivateKey(true);
146
- }, children: "Download private key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => downloadTextFile(`${fileSafe(name)}-certificate.pem`, generated.certificatePem), children: "Download certificate" })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: savedPrivateKey, disabled: !downloadedPrivateKey, onChange: (e) => setSavedPrivateKey(e.target.checked) }), "I\u2019ve saved the private key somewhere safe"] }), !downloadedPrivateKey && (_jsx("p", { className: "text-xs text-text-muted", children: "Download the private key before continuing." }))] }))] })), mode === "existing" && scopeForm, mode === "generate" && generated && savedPrivateKey && scopeForm] }));
152
+ return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setConfirmingDownload(true), children: "Download private key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => downloadTextFile(`${fileSafe(name)}-certificate.pem`, generated.certificatePem), children: "Download certificate" })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: savedPrivateKey, disabled: !downloadedPrivateKey, onChange: (e) => setSavedPrivateKey(e.target.checked) }), "I\u2019ve saved the private key somewhere safe"] }), !downloadedPrivateKey && (_jsx("p", { className: "text-xs text-text-muted", children: "Download the private key before continuing." })), _jsxs(Modal, { open: confirmingDownload, onClose: () => setConfirmingDownload(false), title: "Download the private key?", children: [_jsxs("p", { className: "text-sm mb-3", children: ["The file is ", _jsx("strong", { children: "not encrypted" }), ": it lands in this computer\u2019s downloads folder (and its download history), and anyone who obtains it can decrypt every mailbox covered by this escrow scope on their own - the number of required holders does not apply to a copy of the key."] }), _jsx("p", { className: "text-sm mb-5", children: "Move it straight to offline storage or a password manager your escrow holders control, and delete it from here." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDownload(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
153
+ downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
154
+ setDownloadedPrivateKey(true);
155
+ setConfirmingDownload(false);
156
+ }, children: "Download unencrypted key" })] })] })] }))] })), mode === "existing" && scopeForm, mode === "generate" && generated && savedPrivateKey && scopeForm] }));
147
157
  }
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  import { useEffect, useRef, useState } from "react";
7
7
  import { useRouter } from "@rapidrest/react/client";
8
8
  import { HiCheck } from "react-icons/hi2";
9
- import { ApiRequestError } from "../../../../../lib/util/api.js";
9
+ import { actionErrorMessage } from "../elevation.js";
10
10
  import { getRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
11
11
  import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
12
12
  import { createDomain, listDomains } from "../../../../../lib/admin/domainsApi.js";
@@ -15,7 +15,8 @@ import { getBranding } from "../../../../../lib/branding/brandingApi.js";
15
15
  import { getEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
16
16
  import { listMailboxes } from "../../../../../lib/mail/mailApi.js";
17
17
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
18
- import Alert from "../../../../../lib/components/feedback/Alert.js";
18
+ import { LIST_PAGE_SIZE, listAllPages } from "../../../mail/listAllPages.js";
19
+ import Alert from "../ActionAlert.js";
19
20
  import Button from "../../../../../lib/components/buttons/Button.js";
20
21
  import FormField from "../../../../../lib/components/forms/FormField.js";
21
22
  import Modal from "../../../../../lib/components/overlays/Modal.js";
@@ -43,7 +44,7 @@ function isStepId(value) {
43
44
  return SETUP_STEPS.some((step) => step.id === value);
44
45
  }
45
46
  function errorMessage(err, fallback) {
46
- return err instanceof ApiRequestError ? err.message : fallback;
47
+ return actionErrorMessage(err, fallback);
47
48
  }
48
49
  /**
49
50
  * The first-run setup wizard. Each step saves as the administrator works through it; the wizard itself only records
@@ -203,9 +204,10 @@ function MailboxesStep({ userUid, domain }) {
203
204
  const [error, setError] = useState(null);
204
205
  const [formKey, setFormKey] = useState(0);
205
206
  useEffect(() => {
206
- // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
207
- listMailboxes({ limit: 100, scope: "admin" }, client)
208
- .then((list) =>
207
+ // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own. All pages of them: the
208
+ // administrator's own mailbox may be past the first, and would then be offered to be created again.
209
+ listAllPages((page) => listMailboxes({ limit: LIST_PAGE_SIZE, page, scope: "admin" }, client))
210
+ .then(({ items: list }) =>
209
211
  // Keep any mailbox created here while the list was loading.
210
212
  setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]))
211
213
  .catch((err) => setError(`${errorMessage(err, "Could not load the existing mailboxes.")} Check the Mailboxes page before creating your own, so you don't create it twice.`))
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { authApiFetch } from "../../../../lib/util/api.js";
6
+ import { resetNotifications } from "../../notifications/store.js";
6
7
  /**
7
8
  * The cross-tab sign-out channel - must stay identical to `SIGN_OUT_CHANNEL` in
8
9
  * `apps/shared/search/localIndexRpcClient.ts` (the channel `AppShell` listens on and `destroyAllLocalIndexes()`
@@ -69,6 +70,8 @@ async function logOutOfAuthServer(authServerUrl) {
69
70
  */
70
71
  export async function signOutOfConsole(authServerUrl) {
71
72
  markLocalIndexesForDeletion();
73
+ // The tab's notification history is the session's, and its storage outlives it.
74
+ resetNotifications();
72
75
  broadcastSignOut();
73
76
  await logOutOfAuthServer(authServerUrl);
74
77
  window.location.href = authServerUrl ?? "/";
@@ -4,7 +4,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useRef, useState } from "react";
7
- import { ApiRequestError } from "../../../../lib/util/api.js";
7
+ import { actionErrorMessage } from "./elevation.js";
8
8
  import Button from "../../../../lib/components/buttons/Button.js";
9
9
  /** Page size for the async-request lists (the server returns them newest first and caps `limit` at 500). */
10
10
  export const REQUEST_LIST_PAGE_SIZE = 50;
@@ -30,7 +30,7 @@ export function usePagedList(fetchPage, errorMessage) {
30
30
  const loaded = useRef(items);
31
31
  loaded.current = items;
32
32
  function describe(err) {
33
- return err instanceof ApiRequestError ? err.message : errorMessage;
33
+ return actionErrorMessage(err, errorMessage);
34
34
  }
35
35
  async function reload() {
36
36
  const current = ++generation.current;
@@ -25,7 +25,8 @@ export function linkFromInput(input) {
25
25
  if (/\s/.test(text)) {
26
26
  return undefined;
27
27
  }
28
- if (/^[a-z][a-z0-9+.-]*:/i.test(text)) {
28
+ // "example.com:8080/x" and "localhost:3000" name a host and a port, not a scheme.
29
+ if (/^[a-z][a-z0-9+.-]*:/i.test(text) && !/^(?:localhost|[^\s:/@]+\.[^\s:/@]+):\d{1,5}(?:[/?#]|$)/i.test(text)) {
29
30
  return safeDescriptionHref(text);
30
31
  }
31
32
  return safeDescriptionHref(/^[^\s@/]+@[^\s@/]+$/.test(text) ? `mailto:${text}` : `https://${text}`);
@@ -14,6 +14,8 @@ export interface EventDetailsProps {
14
14
  isOwnAddress?: (address: string) => boolean;
15
15
  calendarName?: string;
16
16
  calendarColor?: string;
17
+ /** Whether the reader may change this mailbox's events: `false` for one shared with them view-only, which gets no Modify, Delete or answer. Default `true`. */
18
+ canWrite?: boolean;
17
19
  onClose: () => void;
18
20
  /** Modify: switch to the edit form. */
19
21
  onModify: () => void;
@@ -30,4 +32,4 @@ export interface EventDetailsProps {
30
32
  * 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 -
31
33
  * its time and that it is busy - with nothing to change, delete or answer.
32
34
  */
33
- export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;
35
+ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, canWrite, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;
@@ -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) {