@rapidmx/web-client 0.29.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 (407) 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 +58 -8
  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/SwipeRow.tsx +26 -4
  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/gestures/useLongPress.ts +138 -0
  102. package/apps/shared/mail/decryptedMessages.ts +51 -2
  103. package/apps/shared/mail/listSnapshots.ts +2 -0
  104. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  105. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  107. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  108. package/apps/shared/notifications/apiErrors.ts +2 -0
  109. package/apps/shared/notifications/preferences.ts +1 -1
  110. package/apps/shared/notifications/store.ts +3 -1
  111. package/apps/shared/plugins/pluginNav.ts +4 -2
  112. package/apps/www/calendar/index.tsx +24 -12
  113. package/apps/www/contacts/[uid].tsx +6 -1
  114. package/apps/www/contacts/index.tsx +66 -12
  115. package/apps/www/index.tsx +139 -43
  116. package/apps/www/messages/[uid].tsx +8 -3
  117. package/apps/www/settings/auto-reply/index.tsx +13 -0
  118. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  119. package/apps/www/settings/encryption/index.tsx +66 -4
  120. package/apps/www/settings/privacy/index.tsx +4 -1
  121. package/apps/www/settings/profile/index.tsx +12 -1
  122. package/apps/www/settings/sharing/index.tsx +11 -1
  123. package/apps/www/settings/signatures/[uid].tsx +6 -4
  124. package/apps/www/tasks/index.tsx +97 -29
  125. package/dist/apps/admin/audit-log/index.js +3 -3
  126. package/dist/apps/admin/branding/index.js +3 -3
  127. package/dist/apps/admin/data-requests/index.js +17 -7
  128. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  129. package/dist/apps/admin/distribution-lists/index.js +3 -3
  130. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  131. package/dist/apps/admin/domains/[uid].js +6 -5
  132. package/dist/apps/admin/domains/index.js +3 -3
  133. package/dist/apps/admin/domains/new/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  135. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  136. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  137. package/dist/apps/admin/index.js +4 -4
  138. package/dist/apps/admin/ingest-queue/index.js +3 -3
  139. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  140. package/dist/apps/admin/quarantine/index.js +4 -4
  141. package/dist/apps/admin/retention-policy/index.js +3 -3
  142. package/dist/apps/admin/signing-certificates/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  144. package/dist/apps/admin/transport-rules/index.js +3 -3
  145. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  146. package/dist/apps/escrow/matters/[uid].js +8 -3
  147. package/dist/apps/escrow/matters/new/index.js +4 -0
  148. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  149. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  150. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  151. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  152. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  153. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  157. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  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 +26 -12
  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.d.ts +8 -3
  220. package/dist/apps/shared/components/mail/ConversationList.js +51 -8
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  222. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  223. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  224. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  225. package/dist/apps/shared/components/mail/SwipeRow.d.ts +5 -1
  226. package/dist/apps/shared/components/mail/SwipeRow.js +17 -3
  227. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  229. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  230. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  231. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  232. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  234. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  235. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  237. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  238. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  239. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  240. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  241. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  242. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  243. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  244. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  246. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  247. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  248. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  249. package/dist/apps/shared/gestures/useLongPress.d.ts +41 -0
  250. package/dist/apps/shared/gestures/useLongPress.js +112 -0
  251. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  252. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  253. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  254. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  255. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  256. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  257. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  258. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  259. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  260. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  261. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  262. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  263. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  264. package/dist/apps/shared/notifications/preferences.js +1 -1
  265. package/dist/apps/shared/notifications/store.js +3 -1
  266. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  267. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  268. package/dist/apps/www/calendar/index.js +24 -12
  269. package/dist/apps/www/contacts/[uid].js +6 -2
  270. package/dist/apps/www/contacts/index.js +62 -11
  271. package/dist/apps/www/index.js +124 -40
  272. package/dist/apps/www/messages/[uid].js +8 -3
  273. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  274. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  275. package/dist/apps/www/settings/encryption/index.js +29 -5
  276. package/dist/apps/www/settings/privacy/index.js +3 -2
  277. package/dist/apps/www/settings/profile/index.js +4 -1
  278. package/dist/apps/www/settings/sharing/index.js +10 -1
  279. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  280. package/dist/apps/www/tasks/index.js +94 -32
  281. package/dist/lib/admin/auditLogApi.js +2 -2
  282. package/dist/lib/admin/distributionListsApi.js +2 -2
  283. package/dist/lib/admin/domainsApi.js +2 -2
  284. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  285. package/dist/lib/admin/escrowScopesApi.js +2 -2
  286. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  287. package/dist/lib/admin/pluginsApi.js +8 -0
  288. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  289. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  290. package/dist/lib/admin/transportRulesApi.js +2 -2
  291. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  292. package/dist/lib/appearance/preferencesApi.js +3 -14
  293. package/dist/lib/auth/session.d.ts +51 -12
  294. package/dist/lib/auth/session.js +214 -38
  295. package/dist/lib/branding/brandingApi.d.ts +3 -3
  296. package/dist/lib/branding/brandingApi.js +9 -21
  297. package/dist/lib/calendar/calendarApi.js +3 -1
  298. package/dist/lib/calendar/eventSearch.js +43 -6
  299. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  300. package/dist/lib/calendar/freeBusyApi.js +13 -5
  301. package/dist/lib/calendar/recurrence.js +51 -3
  302. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  303. package/dist/lib/calendar/useDayKey.js +43 -0
  304. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  305. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  306. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  307. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  308. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  309. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  310. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  311. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  312. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  313. package/dist/lib/components/menus/MenuButton.js +219 -0
  314. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  315. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  316. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  317. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  318. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  319. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  320. package/dist/lib/contacts/contactsApi.js +16 -19
  321. package/dist/lib/contacts/gravatar.d.ts +14 -3
  322. package/dist/lib/contacts/gravatar.js +42 -5
  323. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  324. package/dist/lib/contacts/preparePhoto.js +250 -0
  325. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  326. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  327. package/dist/lib/contacts/vcard.d.ts +3 -0
  328. package/dist/lib/contacts/vcard.js +59 -7
  329. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  330. package/dist/lib/crypto/escrowKeys.js +20 -1
  331. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  332. package/dist/lib/crypto/messageSecurity.js +23 -5
  333. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  334. package/dist/lib/crypto/passwordUnlock.js +13 -2
  335. package/dist/lib/crypto/smime.js +12 -2
  336. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  337. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  338. package/dist/lib/mail/compose/quotedImages.js +35 -9
  339. package/dist/lib/mail/labelsApi.js +2 -2
  340. package/dist/lib/mail/mailApi.d.ts +8 -5
  341. package/dist/lib/mail/mailApi.js +73 -39
  342. package/dist/lib/mail/mailDetailHooks.js +11 -3
  343. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  344. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  345. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  346. package/dist/lib/mail/mailboxImportApi.js +3 -15
  347. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  348. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  349. package/dist/lib/search/queryGrammar.js +12 -4
  350. package/dist/lib/search/searchTier3.d.ts +6 -1
  351. package/dist/lib/search/searchTier3.js +54 -25
  352. package/dist/lib/tasks/tasksApi.js +3 -3
  353. package/dist/lib/util/api.d.ts +26 -4
  354. package/dist/lib/util/api.js +73 -16
  355. package/dist/lib/util/apiQuery.d.ts +11 -0
  356. package/dist/lib/util/apiQuery.js +11 -0
  357. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  358. package/dist/lib/util/settleWithLimit.js +25 -0
  359. package/lib/admin/auditLogApi.ts +2 -2
  360. package/lib/admin/distributionListsApi.ts +2 -2
  361. package/lib/admin/domainsApi.ts +2 -2
  362. package/lib/admin/escrowAuditLogApi.ts +2 -2
  363. package/lib/admin/escrowScopesApi.ts +2 -2
  364. package/lib/admin/pluginsApi.ts +21 -2
  365. package/lib/admin/retentionPolicyApi.ts +4 -0
  366. package/lib/admin/transportRulesApi.ts +2 -2
  367. package/lib/appearance/preferencesApi.ts +3 -14
  368. package/lib/auth/session.ts +239 -39
  369. package/lib/branding/brandingApi.ts +9 -21
  370. package/lib/calendar/calendarApi.ts +3 -1
  371. package/lib/calendar/eventSearch.ts +46 -6
  372. package/lib/calendar/freeBusyApi.ts +14 -6
  373. package/lib/calendar/recurrence.ts +59 -8
  374. package/lib/calendar/useDayKey.ts +45 -0
  375. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  376. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  377. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  378. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  379. package/lib/components/menus/MenuButton.tsx +408 -0
  380. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  381. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  382. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  383. package/lib/contacts/contactsApi.ts +18 -19
  384. package/lib/contacts/gravatar.ts +47 -5
  385. package/lib/contacts/preparePhoto.ts +273 -0
  386. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  387. package/lib/contacts/vcard.ts +60 -7
  388. package/lib/crypto/escrowKeys.ts +21 -1
  389. package/lib/crypto/messageSecurity.ts +33 -9
  390. package/lib/crypto/passwordUnlock.ts +14 -2
  391. package/lib/crypto/smime.ts +13 -2
  392. package/lib/mail/compose/composeQuoting.ts +3 -2
  393. package/lib/mail/compose/quotedImages.ts +39 -9
  394. package/lib/mail/labelsApi.ts +2 -2
  395. package/lib/mail/mailApi.ts +96 -39
  396. package/lib/mail/mailDetailHooks.ts +11 -3
  397. package/lib/mail/mailFilterRulesApi.ts +2 -2
  398. package/lib/mail/mailSignaturesApi.ts +2 -2
  399. package/lib/mail/mailboxImportApi.ts +9 -15
  400. package/lib/mail/messageBodySanitizer.ts +47 -1
  401. package/lib/search/queryGrammar.ts +13 -4
  402. package/lib/search/searchTier3.ts +65 -32
  403. package/lib/tasks/tasksApi.ts +3 -3
  404. package/lib/util/api.ts +99 -16
  405. package/lib/util/apiQuery.ts +13 -0
  406. package/lib/util/settleWithLimit.ts +25 -0
  407. package/package.json +1 -1
@@ -3,16 +3,18 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { FormEvent, useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import { createEscrowScope, EscrowScope, listEscrowScopes } from "../../../../../lib/admin/escrowScopesApi.js";
8
7
  import { generateEscrowKeyPair, GeneratedEscrowKeys } from "../../../../../lib/crypto/escrowKeys.js";
9
8
  import { toDatetimeLocal } from "../../../../../lib/util/dateInput.js";
10
- import Alert from "../../../../../lib/components/feedback/Alert.js";
9
+ import Alert from "../ActionAlert.js";
11
10
  import Button from "../../../../../lib/components/buttons/Button.js";
12
11
  import FormField from "../../../../../lib/components/forms/FormField.js";
12
+ import Modal from "../../../../../lib/components/overlays/Modal.js";
13
+ import { actionErrorMessage } from "../elevation.js";
13
14
  import EscrowScopeKeyAndHoldersFields, {
14
15
  emptyEscrowScopeKeyAndHoldersValue,
15
16
  EscrowScopeKeyAndHoldersValue,
17
+ keyValidityError,
16
18
  selfAsHolderError,
17
19
  } from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
18
20
 
@@ -54,6 +56,8 @@ export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
54
56
  const [generated, setGenerated] = useState<GeneratedEscrowKeys | null>(null);
55
57
  const [downloadedPrivateKey, setDownloadedPrivateKey] = useState(false);
56
58
  const [savedPrivateKey, setSavedPrivateKey] = useState(false);
59
+ // The private key leaves the browser as a plain file, so it is only offered after the administrator has read what that means.
60
+ const [confirmingDownload, setConfirmingDownload] = useState(false);
57
61
  const [keyAndHolders, setKeyAndHolders] = useState<EscrowScopeKeyAndHoldersValue>(emptyEscrowScopeKeyAndHoldersValue());
58
62
  const [error, setError] = useState<string | null>(null);
59
63
  const [busy, setBusy] = useState(false);
@@ -70,6 +74,7 @@ export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
70
74
  setGenerated(null);
71
75
  setDownloadedPrivateKey(false);
72
76
  setSavedPrivateKey(false);
77
+ setConfirmingDownload(false);
73
78
  setKeyAndHolders(emptyEscrowScopeKeyAndHoldersValue());
74
79
  }
75
80
 
@@ -119,6 +124,11 @@ export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
119
124
  setError("The public key, its type, and its fingerprint are all required.");
120
125
  return;
121
126
  }
127
+ const validityError = generated ? null : keyValidityError(keyAndHolders);
128
+ if (validityError) {
129
+ setError(validityError);
130
+ return;
131
+ }
122
132
  if (keyAndHolders.holderUserUids.length === 0) {
123
133
  setError("At least one holder is required.");
124
134
  return;
@@ -145,7 +155,7 @@ export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
145
155
  chooseMode("none");
146
156
  setName("Escrow");
147
157
  } catch (err) {
148
- setError(err instanceof ApiRequestError ? err.message : "Could not create the escrow scope.");
158
+ setError(actionErrorMessage(err, "Could not create the escrow scope."));
149
159
  } finally {
150
160
  setBusy(false);
151
161
  }
@@ -248,10 +258,7 @@ export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
248
258
  <Button
249
259
  type="button"
250
260
  className="!w-auto"
251
- onClick={() => {
252
- downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
253
- setDownloadedPrivateKey(true);
254
- }}
261
+ onClick={() => setConfirmingDownload(true)}
255
262
  >
256
263
  Download private key
257
264
  </Button>
@@ -276,6 +283,32 @@ export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
276
283
  {!downloadedPrivateKey && (
277
284
  <p className="text-xs text-text-muted">Download the private key before continuing.</p>
278
285
  )}
286
+ <Modal open={confirmingDownload} onClose={() => setConfirmingDownload(false)} title="Download the private key?">
287
+ <p className="text-sm mb-3">
288
+ The file is <strong>not encrypted</strong>: it lands in this computer&rsquo;s downloads folder (and its download history),
289
+ and anyone who obtains it can decrypt every mailbox covered by this escrow scope on their own - the number of
290
+ required holders does not apply to a copy of the key.
291
+ </p>
292
+ <p className="text-sm mb-5">
293
+ Move it straight to offline storage or a password manager your escrow holders control, and delete it from here.
294
+ </p>
295
+ <div className="flex gap-3 justify-end">
296
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingDownload(false)}>
297
+ Cancel
298
+ </Button>
299
+ <Button
300
+ type="button"
301
+ className="!w-auto"
302
+ onClick={() => {
303
+ downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
304
+ setDownloadedPrivateKey(true);
305
+ setConfirmingDownload(false);
306
+ }}
307
+ >
308
+ Download unencrypted key
309
+ </Button>
310
+ </div>
311
+ </Modal>
279
312
  </div>
280
313
  )}
281
314
  </div>
@@ -5,7 +5,7 @@
5
5
  import React, { FormEvent, useEffect, useRef, useState } from "react";
6
6
  import { useRouter } from "@rapidrest/react/client";
7
7
  import { HiCheck } from "react-icons/hi2";
8
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
+ import { actionErrorMessage } from "../elevation.js";
9
9
  import { getRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
10
10
  import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
11
11
  import { createDomain, Domain, listDomains } from "../../../../../lib/admin/domainsApi.js";
@@ -14,7 +14,8 @@ import { getBranding } from "../../../../../lib/branding/brandingApi.js";
14
14
  import { EncryptionPolicy, getEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
15
15
  import { listMailboxes, Mailbox } from "../../../../../lib/mail/mailApi.js";
16
16
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
17
- import Alert from "../../../../../lib/components/feedback/Alert.js";
17
+ import { LIST_PAGE_SIZE, listAllPages } from "../../../mail/listAllPages.js";
18
+ import Alert from "../ActionAlert.js";
18
19
  import Button from "../../../../../lib/components/buttons/Button.js";
19
20
  import FormField from "../../../../../lib/components/forms/FormField.js";
20
21
  import Modal from "../../../../../lib/components/overlays/Modal.js";
@@ -50,7 +51,7 @@ function isStepId(value: string | undefined): value is SetupStepId {
50
51
  }
51
52
 
52
53
  function errorMessage(err: unknown, fallback: string): string {
53
- return err instanceof ApiRequestError ? err.message : fallback;
54
+ return actionErrorMessage(err, fallback);
54
55
  }
55
56
 
56
57
  export interface SetupWizardProps {
@@ -397,9 +398,10 @@ function MailboxesStep({ userUid, domain }: { userUid: string; domain?: string }
397
398
  const [formKey, setFormKey] = useState(0);
398
399
 
399
400
  useEffect(() => {
400
- // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
401
- listMailboxes({ limit: 100, scope: "admin" }, client)
402
- .then((list) =>
401
+ // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own. All pages of them: the
402
+ // administrator's own mailbox may be past the first, and would then be offered to be created again.
403
+ listAllPages((page) => listMailboxes({ limit: LIST_PAGE_SIZE, page, scope: "admin" }, client))
404
+ .then(({ items: list }) =>
403
405
  // Keep any mailbox created here while the list was loading.
404
406
  setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]),
405
407
  )
@@ -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
  /**
8
9
  * The cross-tab sign-out channel - must stay identical to `SIGN_OUT_CHANNEL` in
@@ -73,6 +74,8 @@ async function logOutOfAuthServer(authServerUrl: string | undefined): Promise<vo
73
74
  */
74
75
  export async function signOutOfConsole(authServerUrl: string | undefined): Promise<void> {
75
76
  markLocalIndexesForDeletion();
77
+ // The tab's notification history is the session's, and its storage outlives it.
78
+ resetNotifications();
76
79
  broadcastSignOut();
77
80
  await logOutOfAuthServer(authServerUrl);
78
81
  window.location.href = authServerUrl ?? "/";
@@ -3,7 +3,7 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { useRef, useState } from "react";
6
- import { ApiRequestError } from "../../../../lib/util/api.js";
6
+ import { actionErrorMessage } from "./elevation.js";
7
7
  import Button from "../../../../lib/components/buttons/Button.js";
8
8
 
9
9
  /** Page size for the async-request lists (the server returns them newest first and caps `limit` at 500). */
@@ -51,7 +51,7 @@ export function usePagedList<T extends { uid: string }>(
51
51
  loaded.current = items;
52
52
 
53
53
  function describe(err: unknown): string {
54
- return err instanceof ApiRequestError ? err.message : errorMessage;
54
+ return actionErrorMessage(err, errorMessage);
55
55
  }
56
56
 
57
57
  async function reload() {
@@ -38,7 +38,8 @@ export function linkFromInput(input: string): string | undefined {
38
38
  if (/\s/.test(text)) {
39
39
  return undefined;
40
40
  }
41
- if (/^[a-z][a-z0-9+.-]*:/i.test(text)) {
41
+ // "example.com:8080/x" and "localhost:3000" name a host and a port, not a scheme.
42
+ if (/^[a-z][a-z0-9+.-]*:/i.test(text) && !/^(?:localhost|[^\s:/@]+\.[^\s:/@]+):\d{1,5}(?:[/?#]|$)/i.test(text)) {
42
43
  return safeDescriptionHref(text);
43
44
  }
44
45
  return safeDescriptionHref(/^[^\s@/]+@[^\s@/]+$/.test(text) ? `mailto:${text}` : `https://${text}`);
@@ -57,6 +57,8 @@ export interface EventDetailsProps {
57
57
  isOwnAddress?: (address: string) => boolean;
58
58
  calendarName?: string;
59
59
  calendarColor?: string;
60
+ /** 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`. */
61
+ canWrite?: boolean;
60
62
  onClose: () => void;
61
63
  /** Modify: switch to the edit form. */
62
64
  onModify: () => void;
@@ -81,6 +83,7 @@ export default function EventDetails({
81
83
  isOwnAddress,
82
84
  calendarName,
83
85
  calendarColor,
86
+ canWrite = true,
84
87
  onClose,
85
88
  onModify,
86
89
  onSaved,
@@ -90,6 +93,8 @@ export default function EventDetails({
90
93
  const [error, setError] = useState<string | null>(null);
91
94
  const [busy, setBusy] = useState(false);
92
95
  const [confirmingDelete, setConfirmingDelete] = useState(false);
96
+ // An answer to one occurrence of a series waits here for the reader to confirm: the server records it for the entire series.
97
+ const [pendingResponse, setPendingResponse] = useState<AttendeeResponseInput | null>(null);
93
98
  const [requesting, setRequesting] = useState(false);
94
99
  // The organizer's own join link for the event's video meeting: `undefined` while it is fetched, `null` once known to be none.
95
100
  const [organizerJoinUrl, setOrganizerJoinUrl] = useState<string | null | undefined>(undefined);
@@ -136,6 +141,7 @@ export default function EventDetails({
136
141
  // `@rapidmx/restapi`'s `respond()` route has no occurrence-vs-series scope of its own (see `calendarApi.ts`'s `respondToEvent` doc
137
142
  // comment) - it always acts on `occurrence.uid` as a single event document.
138
143
  async function handleRespond(responseStatus: AttendeeResponseInput) {
144
+ setPendingResponse(null);
139
145
  setError(null);
140
146
  setBusy(true);
141
147
  try {
@@ -349,7 +355,7 @@ export default function EventDetails({
349
355
  </div>
350
356
  )}
351
357
 
352
- {myResponse !== undefined && (
358
+ {myResponse !== undefined && canWrite && (
353
359
  <div className="flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt">
354
360
  <div className="flex items-center justify-between gap-3 flex-wrap">
355
361
  <span className="text-sm font-medium">Your response</span>
@@ -364,7 +370,7 @@ export default function EventDetails({
364
370
  className={["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" ")}
365
371
  aria-pressed={current}
366
372
  disabled={busy}
367
- onClick={() => void handleRespond(response)}
373
+ onClick={() => (occurrence.isRecurringOccurrence ? setPendingResponse(response) : void handleRespond(response))}
368
374
  >
369
375
  {current && <HiOutlineCheck size={14} aria-hidden="true" />}
370
376
  {label}
@@ -373,12 +379,26 @@ export default function EventDetails({
373
379
  })}
374
380
  </div>
375
381
  </div>
382
+ {pendingResponse && (
383
+ <div role="alert" className="flex flex-wrap items-center gap-2 text-sm">
384
+ <span className="flex-1 min-w-0">
385
+ {pendingResponse === "declined" ? "Declining" : "Your answer"} applies to every event in this series, not just this one
386
+ {pendingResponse === "declined" ? ", and removes the series from your calendar" : ""}.
387
+ </span>
388
+ <Button type="button" variant="secondary" className="!w-auto" disabled={busy} onClick={() => setPendingResponse(null)}>
389
+ Cancel
390
+ </Button>
391
+ <Button type="button" className="!w-auto" disabled={busy} onClick={() => void handleRespond(pendingResponse)}>
392
+ {answers.find((a) => a.response === pendingResponse)!.label} the series
393
+ </Button>
394
+ </div>
395
+ )}
376
396
  <p className="text-xs text-text-muted">Other attendees&rsquo; responses may take a few minutes to update.</p>
377
397
  </div>
378
398
  )}
379
399
 
380
400
  <div className="flex flex-wrap items-center gap-3 px-5 pb-4">
381
- {!isInvited && (
401
+ {!isInvited && canWrite && (
382
402
  <Button type="button" className="!w-auto" disabled={busy} onClick={onModify}>
383
403
  Modify
384
404
  </Button>
@@ -388,14 +408,20 @@ export default function EventDetails({
388
408
  {canChange ? "Request a change" : "Add guests"}
389
409
  </Button>
390
410
  )}
391
- {!occurrence.isRecurringOccurrence ? (
392
- <Button type="button" variant="secondary" className="!w-auto text-danger" disabled={busy} onClick={() => void handleDelete("series")}>
393
- Delete
394
- </Button>
395
- ) : !confirmingDelete ? (
411
+ {!canWrite ? null : !confirmingDelete ? (
396
412
  <Button type="button" variant="secondary" className="!w-auto text-danger" disabled={busy} onClick={() => setConfirmingDelete(true)}>
397
413
  Delete
398
414
  </Button>
415
+ ) : !occurrence.isRecurringOccurrence ? (
416
+ <div role="alert" className="flex items-center gap-2">
417
+ <span className="text-sm">Delete this event?</span>
418
+ <Button type="button" variant="secondary" className="!w-auto text-danger" disabled={busy} onClick={() => void handleDelete("series")}>
419
+ Yes, delete
420
+ </Button>
421
+ <Button type="button" variant="secondary" className="!w-auto" disabled={busy} onClick={() => setConfirmingDelete(false)}>
422
+ Keep
423
+ </Button>
424
+ </div>
399
425
  ) : (
400
426
  <div className="flex gap-2">
401
427
  <Button type="button" variant="secondary" className="!w-auto text-danger" disabled={busy} onClick={() => void handleDelete("occurrence")}>
@@ -2,7 +2,7 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, MutableRefObject, useEffect, useMemo, useState } from "react";
5
+ import React, { FormEvent, MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { format } from "date-fns";
7
7
  import { ApiClient, ApiRequestError } from "../../../../lib/util/api.js";
8
8
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
@@ -15,6 +15,7 @@ import {
15
15
  guestPermissionsOf,
16
16
  updateCalendarEvent,
17
17
  visibilityOf,
18
+ WeekdayCode,
18
19
  } from "../../../../lib/calendar/calendarApi.js";
19
20
  import {
20
21
  VideoMeetingInvitee,
@@ -71,19 +72,27 @@ function wallTimeOfDayMs(instantMs: number, timezone: string | undefined): numbe
71
72
  return ((wall % MS_PER_DAY) + MS_PER_DAY) % MS_PER_DAY;
72
73
  }
73
74
 
75
+ /** 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. */
76
+ function seriesDateKey(iso: string, allDay: boolean, timezone: string | undefined): string {
77
+ return allDay ? allDayDateKey(iso) : new Date(toEventWallClock(new Date(iso).getTime(), timezone, false)).toISOString().slice(0, 10);
78
+ }
79
+
80
+ const WEEKDAY_ORDER: WeekdayCode[] = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
81
+
74
82
  /**
75
83
  * Rewrites an edited occurrence's `startDate`/`endDate` for a save to the entire series. The modal only
76
84
  * ever shows (and edits) *this occurrence's* dates, but the series is stored as its master record starting
77
85
  * at the first occurrence - sending the occurrence's dates as-is would move the whole series' start to
78
- * this occurrence. So only the change in time-of-day (and the new duration) is applied to the master's own
79
- * start; the series keeps its original first date. Unchanged times aren't sent at all.
86
+ * this occurrence. So the change made to the occurrence (its date and its time of day) is applied to the
87
+ * master's own start, and the new duration is kept: moving a Tuesday occurrence to Thursday moves the series
88
+ * by two days, never silently dropping the change. Unchanged times aren't sent at all.
80
89
  *
81
- * Times of day are read on the event's own timezone wall clock (the series expands there), not the
82
- * browser's, and the change is the smallest signed difference modulo 24h - moving 23:00 to 01:00 is two
83
- * hours later (into the next day), never 22 hours earlier. The master's new start is converted back from
84
- * that wall clock, so a master on the other side of a DST change keeps the chosen local time.
90
+ * The change is measured on the event's own timezone wall clock (the series expands there), not the
91
+ * browser's. The master's new start is converted back from that wall clock, so a master on the other side of
92
+ * a DST change keeps the chosen local time. A weekly rule that the edit left untouched is rotated by the
93
+ * same number of days, so its weekdays move with the start (a rule the form already changed is left as it is).
85
94
  */
86
- async function toSeriesFields(occurrence: CalendarOccurrence, fields: EventFields, client?: ApiClient): Promise<EventFields> {
95
+ async function toSeriesFields(occurrence: CalendarOccurrence, fields: EventFields, formZone: string, client?: ApiClient): Promise<EventFields> {
87
96
  const { startDate, endDate, ...rest } = fields;
88
97
  if (sameMinute(startDate as string, occurrence.startDate) && sameMinute(endDate as string, occurrence.endDate)) {
89
98
  // `saveEventSeries()` treats *any* `timezone`/`allDay` in the update as a possible reinterpretation of
@@ -96,23 +105,32 @@ async function toSeriesFields(occurrence: CalendarOccurrence, fields: EventField
96
105
  const timezone = fields.timezone as string;
97
106
  const newStart = new Date(startDate as string).getTime();
98
107
  const durationMs = new Date(endDate as string).getTime() - newStart;
108
+ // 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.
109
+ const frame = fields.allDay || occurrence.allDay ? formZone : timezone;
110
+ const dayShift = Math.round(
111
+ (Date.parse(seriesDateKey(startDate as string, !!fields.allDay, frame)) - Date.parse(seriesDateKey(occurrence.startDate, occurrence.allDay, frame))) / MS_PER_DAY,
112
+ );
99
113
  let masterStart: number;
100
114
  if (fields.allDay) {
101
- // Timed -> all-day keeps the master's own date on its wall clock.
115
+ // The master's own date (on its wall clock when it was timed), moved by the days the occurrence moved.
102
116
  const masterDateKey = master.allDay
103
117
  ? allDayDateKey(master.startDate)
104
118
  : new Date(toEventWallClock(new Date(master.startDate).getTime(), timezone, false)).toISOString().slice(0, 10);
105
- masterStart = new Date(allDayInstant(masterDateKey)).getTime();
119
+ masterStart = new Date(allDayInstant(addDaysToKey(masterDateKey, dayShift))).getTime();
106
120
  } else if (master.allDay) {
107
- // All-day -> timed: the master's own date at the newly chosen wall-clock time.
108
- const masterDateWall = new Date(allDayInstant(allDayDateKey(master.startDate))).getTime();
121
+ // All-day -> timed: the master's own date (moved by the days the occurrence moved) at the newly chosen wall-clock time.
122
+ const masterDateWall = new Date(allDayInstant(addDaysToKey(allDayDateKey(master.startDate), dayShift))).getTime();
109
123
  masterStart = fromEventWallClock(masterDateWall + wallTimeOfDayMs(newStart, timezone), timezone, false);
110
124
  } else {
111
- const rawDelta = wallTimeOfDayMs(newStart, timezone) - wallTimeOfDayMs(new Date(occurrence.startDate).getTime(), timezone);
112
- const delta = ((((rawDelta + MS_PER_DAY / 2) % MS_PER_DAY) + MS_PER_DAY) % MS_PER_DAY) - MS_PER_DAY / 2;
125
+ const delta = toEventWallClock(newStart, timezone, false) - toEventWallClock(new Date(occurrence.startDate).getTime(), timezone, false);
113
126
  const masterWall = toEventWallClock(new Date(master.startDate).getTime(), timezone, false);
114
127
  masterStart = fromEventWallClock(masterWall + delta, timezone, false);
115
128
  }
129
+ const rule = rest.recurrenceRule;
130
+ const original = occurrence.recurrenceRule?.byDay;
131
+ if (rule?.freq === "weekly" && rule.byDay && original && dayShift % 7 !== 0 && rule.byDay.join() === original.join()) {
132
+ rest.recurrenceRule = { ...rule, byDay: rule.byDay.map((day) => WEEKDAY_ORDER[(WEEKDAY_ORDER.indexOf(day) + (dayShift % 7) + 7) % 7]) };
133
+ }
116
134
  return {
117
135
  ...rest,
118
136
  startDate: new Date(masterStart).toISOString(),
@@ -220,6 +238,8 @@ export default function EventEditor({
220
238
  targetFolderUid: folderUid,
221
239
  };
222
240
  });
241
+ // What an existing event's form held when it opened, to tell whether anything was changed since.
242
+ const [pristine] = useState(() => JSON.stringify(values));
223
243
  const { attendees, location, videoEnabled } = values;
224
244
  function update(patch: Partial<EventFormValues>) {
225
245
  setValues((prev) => ({ ...prev, ...patch }));
@@ -245,6 +265,12 @@ export default function EventEditor({
245
265
  // finish the video-conferencing step after it: the modal then stays open on that error, and the retry
246
266
  // must update *that* record (its uid and its new version), not re-create it or reuse the stale prop.
247
267
  const [savedEvent, setSavedEvent] = useState<CalendarEvent | null>(null);
268
+ // 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
269
+ // must not send them - the first save already applied any change of time.
270
+ const savedAsSeries = useRef(false);
271
+ // 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,
272
+ // which would send the invitees a second set of join links and leave this one orphaned.
273
+ const mintedMeeting = useRef<{ uid: string; organizerJoinUrl: string | null } | null>(null);
248
274
  const [editScope, setEditScope] = useState<EditScope>("occurrence");
249
275
  const [error, setError] = useState<string | null>(null);
250
276
  const [saving, setSaving] = useState(false);
@@ -277,7 +303,9 @@ export default function EventEditor({
277
303
  // "This event only" detaches a standalone, non-repeating copy - the series' rule doesn't apply to it.
278
304
  const editingSingleOccurrence = !!occurrence?.isRecurringOccurrence && editScope === "occurrence";
279
305
 
280
- dirtyRef.current = !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
306
+ dirtyRef.current = occurrence
307
+ ? JSON.stringify(values) !== pristine
308
+ : !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
281
309
 
282
310
  function updateAttendee(index: number, patch: Partial<Attendee>) {
283
311
  setValues((prev) => ({ ...prev, attendees: prev.attendees.map((a, i) => (i === index ? { ...a, ...patch } : a)) }));
@@ -295,6 +323,12 @@ export default function EventEditor({
295
323
  }));
296
324
  }
297
325
 
326
+ // The weekday the form's start falls on, in the frame the series expands in (`undefined` while the start is not a valid date).
327
+ function startWeekdayOf(v: Pick<EventFormValues, "start" | "allDay" | "formZone" | "timezone">): WeekdayCode | undefined {
328
+ const ms = v.allDay ? Number.NaN : wallStringToMs(v.start, v.formZone, deviceZone);
329
+ return startWeekdayCode(v.allDay ? v.start : Number.isNaN(ms) ? "" : new Date(ms).toISOString(), v.allDay, v.timezone);
330
+ }
331
+
298
332
  // Moving the start moves the end with it, so the event keeps its length (a Google Calendar habit, and the way to
299
333
  // reschedule without touching two fields).
300
334
  function setStart(next: string) {
@@ -309,7 +343,13 @@ export default function EventEditor({
309
343
  ? `${new Date(endMs + shift).toISOString().slice(0, 10)}T00:00`
310
344
  : msToWallString(endMs + shift, prev.formZone, deviceZone);
311
345
  }
312
- return { ...prev, start: next, end: nextEnd };
346
+ // A weekly rule on just the start's weekday ("Weekly on Wednesday") follows the start to its new weekday - left alone it
347
+ // would not include the event's own first day, and the series would begin on the next of the old weekday instead.
348
+ const oldDay = startWeekdayOf(prev);
349
+ const nextDay = startWeekdayOf({ ...prev, start: next });
350
+ const rule = prev.recurrenceRule;
351
+ const followsStart = oldDay && nextDay && rule?.freq === "weekly" && rule.byDay?.length === 1 && rule.byDay[0] === oldDay;
352
+ return { ...prev, start: next, end: nextEnd, recurrenceRule: followsStart ? { ...rule, byDay: [nextDay] } : rule };
313
353
  });
314
354
  }
315
355
 
@@ -412,29 +452,34 @@ export default function EventEditor({
412
452
  setVideoError("Add at least one attendee other than yourself to add video conferencing.");
413
453
  return { event: saved, failed: true };
414
454
  }
415
- const result = await createVideoMeeting(
416
- {
417
- mailboxUid: saved.mailboxUid,
418
- title: saved.title.slice(0, MAX_MEETING_TITLE_LENGTH),
419
- visibility: "private",
420
- calendarEventUid: saved.uid,
421
- startTime: saved.startDate,
422
- endTime: saved.endDate,
423
- invitees,
424
- },
425
- client,
426
- );
455
+ if (!mintedMeeting.current) {
456
+ const result = await createVideoMeeting(
457
+ {
458
+ mailboxUid: saved.mailboxUid,
459
+ title: saved.title.slice(0, MAX_MEETING_TITLE_LENGTH),
460
+ visibility: "private",
461
+ calendarEventUid: saved.uid,
462
+ startTime: saved.startDate,
463
+ endTime: saved.endDate,
464
+ invitees,
465
+ },
466
+ client,
467
+ );
468
+ mintedMeeting.current = { uid: result.meeting.uid, organizerJoinUrl: result.organizerJoinUrl ?? null };
469
+ }
470
+ const meeting = mintedMeeting.current;
427
471
  const patched = await updateCalendarEvent(
428
472
  {
429
473
  uid: saved.uid,
430
474
  version: saved.version,
431
475
  location: VIDEO_LOCATION_PLACEHOLDER,
432
- videoMeetingUid: result.meeting.uid,
476
+ videoMeetingUid: meeting.uid,
433
477
  },
434
478
  client,
435
479
  );
436
- setVideoMeetingUid(result.meeting.uid);
437
- setOrganizerJoinUrl(result.organizerJoinUrl ?? null);
480
+ mintedMeeting.current = null;
481
+ setVideoMeetingUid(meeting.uid);
482
+ setOrganizerJoinUrl(meeting.organizerJoinUrl);
438
483
  update({ location: VIDEO_LOCATION_PLACEHOLDER });
439
484
  return { event: patched, failed: false };
440
485
  }
@@ -546,8 +591,17 @@ export default function EventEditor({
546
591
  // A retry after the event was stored but its video meeting couldn't be (see `savedEvent`):
547
592
  // update exactly the record the first attempt left behind, whichever branch below created it.
548
593
  // A series' own exception/detached-occurrence re-pointing already ran on that attempt and has
549
- // nothing left to shift - the dates being sent again are the ones it moved everything to.
550
- saved = await updateCalendarEvent({ uid: savedEvent.uid, version: savedEvent.version, ...(fields as Partial<CalendarEventInput>) }, client);
594
+ // nothing left to shift. For a series the form's dates are those of the edited occurrence, not
595
+ // the master's: sending them would move the whole series to that occurrence, so they (and the
596
+ // zone and all-day flag they go with) stay as the first attempt left them.
597
+ const retryFields: EventFields = { ...fields };
598
+ if (savedAsSeries.current) {
599
+ delete retryFields.startDate;
600
+ delete retryFields.endDate;
601
+ delete retryFields.timezone;
602
+ delete retryFields.allDay;
603
+ }
604
+ saved = await updateCalendarEvent({ uid: savedEvent.uid, version: savedEvent.version, ...(retryFields as Partial<CalendarEventInput>) }, client);
551
605
  } else if (!occurrence) {
552
606
  // The organizer is only ever set on create - an edit keeps the event's own organizer.
553
607
  saved = await createCalendarEvent(
@@ -564,10 +618,11 @@ export default function EventEditor({
564
618
  } else if (occurrence.isRecurringOccurrence) {
565
619
  const series = await saveEventSeries(
566
620
  occurrence,
567
- (await toSeriesFields(occurrence, fields, client)) as Partial<CalendarEventInput>,
621
+ (await toSeriesFields(occurrence, fields, values.formZone, client)) as Partial<CalendarEventInput>,
568
622
  client,
569
623
  );
570
624
  saved = series;
625
+ savedAsSeries.current = true;
571
626
  detachedSyncFailed = !!series.detachedOccurrenceSyncFailed;
572
627
  } else {
573
628
  saved = await updateCalendarEvent({ uid: occurrence.uid, version: occurrence.version, ...(fields as Partial<CalendarEventInput>) }, client);
@@ -601,7 +656,6 @@ export default function EventEditor({
601
656
  const joinUrl = joinMeetingUrl(organizerJoinUrl);
602
657
 
603
658
  const timeZones = useMemo(() => timeZoneOptions(values.formZone, values.timezone, deviceZone), [values.formZone, values.timezone, deviceZone]);
604
- const startMs = values.allDay ? Number.NaN : wallStringToMs(values.start, values.formZone, deviceZone);
605
659
  const calendarFolderUid = occurrence ? occurrence.folderUid : values.targetFolderUid;
606
660
 
607
661
  const controller: EventFormController = {
@@ -615,11 +669,7 @@ export default function EventEditor({
615
669
  timeZones,
616
670
  organizerAddress: effectiveOrganizerAddress,
617
671
  deviceZone,
618
- startWeekday: startWeekdayCode(
619
- values.allDay ? values.start : Number.isNaN(startMs) ? "" : new Date(startMs).toISOString(),
620
- values.allDay,
621
- values.timezone,
622
- ),
672
+ startWeekday: startWeekdayOf(values),
623
673
  editingSingleOccurrence,
624
674
  updateAttendee,
625
675
  removeAttendee,
@@ -339,11 +339,14 @@ export function RecurrenceSelect({
339
339
  onChange,
340
340
  allDay,
341
341
  startWeekday,
342
+ startDateKey,
342
343
  }: {
343
344
  value: RecurrenceRule | null;
344
345
  onChange: (rule: RecurrenceRule | null) => void;
345
346
  allDay: boolean;
346
347
  startWeekday: WeekdayCode | undefined;
348
+ /** The event's start date (`yyyy-MM-dd`), for a custom rule's default end date. */
349
+ startDateKey?: string;
347
350
  }) {
348
351
  // Picking "Custom…" for a rule that is also a plain repeat still opens the editor.
349
352
  const [customRequested, setCustomRequested] = useState(false);
@@ -377,7 +380,7 @@ export function RecurrenceSelect({
377
380
  <option value="weekdays">Every weekday (Monday to Friday)</option>
378
381
  <option value="custom">Custom…</option>
379
382
  </select>
380
- {selected === "custom" && <RecurrenceEditor value={value} onChange={onChange} allDay={allDay} startWeekday={startWeekday} hideToggle />}
383
+ {selected === "custom" && <RecurrenceEditor value={value} onChange={onChange} allDay={allDay} startWeekday={startWeekday} startDateKey={startDateKey} hideToggle />}
381
384
  </div>
382
385
  );
383
386
  }
@@ -454,7 +457,7 @@ export function DateTimeControls({ c }: { c: EventFormController }) {
454
457
  All day
455
458
  </label>
456
459
  {!c.editingSingleOccurrence && (
457
- <RecurrenceSelect value={values.recurrenceRule} onChange={(rule) => c.update({ recurrenceRule: rule })} allDay={values.allDay} startWeekday={c.startWeekday} />
460
+ <RecurrenceSelect value={values.recurrenceRule} onChange={(rule) => c.update({ recurrenceRule: rule })} allDay={values.allDay} startWeekday={c.startWeekday} startDateKey={values.start.slice(0, 10)} />
458
461
  )}
459
462
  </div>
460
463
  );
@@ -16,16 +16,28 @@ export interface EventMatchNavProps {
16
16
  noun?: "match" | "event";
17
17
  }
18
18
 
19
+ /** 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. */
20
+ const ARROW_WIDGET_SELECTOR = [
21
+ ...["menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "radiogroup", "radio", "listbox", "option"].map((role) => `[role="${role}"]`),
22
+ ...["slider", "spinbutton", "tablist", "tab", "tree", "grid", "combobox"].map((role) => `[role="${role}"]`),
23
+ '[aria-haspopup][aria-expanded="true"]',
24
+ ].join(",");
25
+
26
+ /** Whether the arrow keys pressed at `target` belong to what is there: a field or select, or a widget with arrows of its own. */
27
+ function takesArrowKeys(target: EventTarget | null): boolean {
28
+ return isTextEntry(target) || (target instanceof Element && target.closest(ARROW_WIDGET_SELECTOR) !== null);
29
+ }
30
+
19
31
  /**
20
32
  * 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
21
33
  * inside the dialog (so Tab reaches them and the dialog's focus trap holds) but are positioned against the window - either side of the
22
34
  * 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
23
- * a field is taking them.
35
+ * a field or a widget (a select, a menu, a slider, ...) is taking them.
24
36
  */
25
37
  export default function EventMatchNav({ onPrevious, onNext, canPrevious = true, canNext = true, noun = "match" }: EventMatchNavProps) {
26
38
  useEffect(() => {
27
39
  function handleKeyDown(event: KeyboardEvent) {
28
- if ((event.key !== "ArrowLeft" && event.key !== "ArrowRight") || event.defaultPrevented || isTextEntry(event.target)) {
40
+ if ((event.key !== "ArrowLeft" && event.key !== "ArrowRight") || event.defaultPrevented || takesArrowKeys(event.target)) {
29
41
  return;
30
42
  }
31
43
  if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {