@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
@@ -8,6 +8,7 @@ import { CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
8
8
  import Modal from "../../../../lib/components/overlays/Modal.js";
9
9
  import Button from "../../../../lib/components/buttons/Button.js";
10
10
  import useIsMobile from "../../../../lib/util/useIsMobile.js";
11
+ import { useMailboxUpdateAccess } from "../../mail/useMailboxUpdateAccess.js";
11
12
  import EventDetails from "./EventDetails.js";
12
13
  import EventEditor from "./EventEditor.js";
13
14
  import EventMatchNav from "./EventMatchNav.js";
@@ -119,6 +120,10 @@ export default function EventModal({
119
120
  const [syncWarning, setSyncWarning] = useState(false);
120
121
  // Whether the form holds anything a click on the backdrop would throw away (kept up to date by the editor).
121
122
  const dirtyRef = useRef(false);
123
+ // Escape or the close button on a form that holds something asks before throwing it away.
124
+ const [confirmingDiscard, setConfirmingDiscard] = useState(false);
125
+ // An event on a calendar shared with the reader view-only cannot be changed, deleted or answered.
126
+ const canWrite = useMailboxUpdateAccess(mailboxOptions?.find((option) => option.mailbox.uid === occurrence?.mailboxUid)?.mailbox);
122
127
 
123
128
  if (!open) {
124
129
  return null;
@@ -168,7 +173,7 @@ export default function EventModal({
168
173
  focusKey={isForm ? layout : "details"}
169
174
  anchor={anchor}
170
175
  width={width}
171
- onClose={isForm ? leaveForm : onClose}
176
+ onClose={isForm ? () => (dirtyRef.current ? setConfirmingDiscard(true) : leaveForm()) : onClose}
172
177
  onBackdropPress={isForm ? () => !dirtyRef.current && leaveForm() : undefined}
173
178
  >
174
179
  {isForm ? (
@@ -214,6 +219,7 @@ export default function EventModal({
214
219
  isOwnAddress={isOwnAddress}
215
220
  calendarName={calendars?.find((cal) => cal.uid === occurrence.folderUid)?.name}
216
221
  calendarColor={folderColors?.[occurrence.folderUid]}
222
+ canWrite={canWrite}
217
223
  onClose={onClose}
218
224
  onModify={() => setEditing(true)}
219
225
  onSaved={onSaved}
@@ -222,6 +228,24 @@ export default function EventModal({
222
228
  </SlideIn>
223
229
  )}
224
230
  {!isForm && matchNav && <EventMatchNav onPrevious={matchNav.onPrevious} onNext={matchNav.onNext} canPrevious={matchNav.canPrevious} canNext={matchNav.canNext} noun={matchNav.noun} />}
231
+ <Modal open={confirmingDiscard} onClose={() => setConfirmingDiscard(false)} title="Discard changes?">
232
+ <p className="text-sm mb-5">What you entered in this event has not been saved.</p>
233
+ <div className="flex gap-3 justify-end">
234
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingDiscard(false)}>
235
+ Keep editing
236
+ </Button>
237
+ <Button
238
+ type="button"
239
+ className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger"
240
+ onClick={() => {
241
+ setConfirmingDiscard(false);
242
+ leaveForm();
243
+ }}
244
+ >
245
+ Discard
246
+ </Button>
247
+ </div>
248
+ </Modal>
225
249
  </EventShell>
226
250
  );
227
251
  }
@@ -105,7 +105,7 @@ export default function FindATime({ c }: { c: EventFormController }) {
105
105
  }, [startDay]);
106
106
 
107
107
  // You, then each guest once (case-insensitively), at most as many as one look-up may ask about.
108
- const rows = useMemo<Row[]>(() => {
108
+ const { rows, skipped } = useMemo<{ rows: Row[]; skipped: number }>(() => {
109
109
  const seen = new Set([organizerAddress.toLowerCase()]);
110
110
  const guests: Row[] = [];
111
111
  for (const attendee of values.attendees) {
@@ -115,7 +115,8 @@ export default function FindATime({ c }: { c: EventFormController }) {
115
115
  guests.push({ address: attendee.address, name: attendee.displayName || attendee.address, you: false });
116
116
  }
117
117
  }
118
- return [{ address: organizerAddress, name: "You", you: true }, ...guests].slice(0, FREE_BUSY_MAX_ADDRESSES);
118
+ const everyone = [{ address: organizerAddress, name: "You", you: true }, ...guests];
119
+ return { rows: everyone.slice(0, FREE_BUSY_MAX_ADDRESSES), skipped: Math.max(0, everyone.length - FREE_BUSY_MAX_ADDRESSES) };
119
120
  }, [values.attendees, organizerAddress]);
120
121
  const guestCount = rows.length - 1;
121
122
  const addresses = rows.map((row) => row.address);
@@ -242,6 +243,13 @@ export default function FindATime({ c }: { c: EventFormController }) {
242
243
  <span className="text-xs text-text-muted">{describeTimeZone(formZone, zoneDate(dayKey))}</span>
243
244
  </div>
244
245
 
246
+ {skipped > 0 && (
247
+ <p className="text-xs text-text-muted">
248
+ {skipped} {skipped === 1 ? "guest" : "guests"} beyond the first {FREE_BUSY_MAX_ADDRESSES - 1} {skipped === 1 ? "isn’t" : "aren’t"} checked, and {skipped === 1 ? "is" : "are"} not
249
+ counted in who is free.
250
+ </p>
251
+ )}
252
+
245
253
  {lookUp.status === "error" && (
246
254
  <div role="alert" className="flex flex-wrap items-center gap-3 text-sm text-danger">
247
255
  <span>{lookUp.message}</span>
@@ -2,14 +2,18 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { useEffect, useMemo, useRef } from "react";
5
+ import React, { memo, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { addDays, format, isToday, parseISO, startOfDay } from "date-fns";
7
7
  import { CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
8
8
  import { allDayDateKey, localDateKey, startsOnDay } from "./allDay.js";
9
9
  import { occurrenceMarker, useActiveOccurrenceKey } from "./activeOccurrence.js";
10
+ import { useDayKey } from "../../../../lib/calendar/useDayKey.js";
10
11
 
11
- /** The most days one event is listed on, so an event that runs for years does not become thousands of rows. */
12
+ /** The most days one event is listed on (counted from today), so an event that runs for years does not become thousands of rows. */
12
13
  export const LIST_MAX_DAYS_PER_EVENT = 366;
14
+ /** The days (that have events) rendered at first, and how many more each time the end of the list is reached. */
15
+ export const LIST_INITIAL_DAYS = 60;
16
+ export const LIST_DAYS_PER_PAGE = 60;
13
17
 
14
18
  /** A step through the list's months: `nonce` makes a second press in the same direction a new step. */
15
19
  export interface ListStep {
@@ -43,8 +47,8 @@ function timeLabel(occurrence: CalendarOccurrence, day: Date): string {
43
47
  return start.getTime() === end.getTime() ? format(start, "h:mma") : `${format(start, "h:mma")} – ${format(end, "h:mma")}`;
44
48
  }
45
49
 
46
- /** The days (local midnights) `occurrence` is on show on: from its first to its last, at most `LIST_MAX_DAYS_PER_EVENT`. */
47
- function daysOf(occurrence: CalendarOccurrence): Date[] {
50
+ /** The days (local midnights) `occurrence` is on show on from `today` (a local midnight) on: its first to its last, at most `LIST_MAX_DAYS_PER_EVENT` of them. */
51
+ function daysOf(occurrence: CalendarOccurrence, today: Date): Date[] {
48
52
  let first: Date;
49
53
  let last: Date;
50
54
  if (occurrence.allDay) {
@@ -58,8 +62,12 @@ function daysOf(occurrence: CalendarOccurrence): Date[] {
58
62
  // An event that ends at midnight does not reach into that day.
59
63
  last = startOfDay(end > start ? new Date(end.getTime() - 1) : start);
60
64
  }
61
- const days = [first];
62
- for (let day = addDays(first, 1); day <= last && days.length < LIST_MAX_DAYS_PER_EVENT; day = addDays(day, 1)) {
65
+ // An event is on its first day at least. The days before today are not listed, so the cap counts from today: an event that began years ago and
66
+ // still runs would otherwise be spent before it reached it.
67
+ const lastDay = last < first ? first : last;
68
+ const from = first < today ? today : first;
69
+ const days: Date[] = [];
70
+ for (let day = from; day <= lastDay && days.length < LIST_MAX_DAYS_PER_EVENT; day = addDays(day, 1)) {
63
71
  days.push(day);
64
72
  }
65
73
  return days;
@@ -76,22 +84,22 @@ function sortedForDay(occurrences: CalendarOccurrence[], day: Date): CalendarOcc
76
84
  * nothing at all says so). A multi-day event is listed on every day it covers, as the month grid shows it. Each row is a full-width button that
77
85
  * opens the event's card, with the time, title, place (or organizer) and its calendar's colour.
78
86
  *
79
- * Every day is rendered (an event is a cheap row, and a series is bounded to 500 occurrences - see `eventSearch.ts`); the days are grouped once per
80
- * change of `occurrences`, not per render. The list scrolls itself to `focusDate` and, on a `step`, to the neighbouring month.
87
+ * The days are grouped once per change of `occurrences` (or of the day), not per render, but only the first `LIST_INITIAL_DAYS` of them are rendered:
88
+ * more follow as the end of the list is scrolled to (a daily series alone is thousands of rows). A jump, a step or a search match that lies beyond
89
+ * the rendered days renders up to it first. The list scrolls itself to `focusDate` and, on a `step`, to the neighbouring month.
81
90
  */
82
- export default function ListView({ occurrences, folderColors, onSelectEvent, focusDate, jumpNonce, step }: ListViewProps) {
91
+ function ListView({ occurrences, folderColors, onSelectEvent, focusDate, jumpNonce, step }: ListViewProps) {
83
92
  const activeKey = useActiveOccurrenceKey();
93
+ // Today moves on at midnight, with the list open or not.
94
+ const dayKey = useDayKey();
84
95
  const scrollerRef = useRef<HTMLDivElement>(null);
96
+ const sentinelRef = useRef<HTMLDivElement>(null);
85
97
  const groups = useMemo(() => {
86
98
  const byDay = new Map<string, { day: Date; items: CalendarOccurrence[] }>();
87
- const todayKey = localDateKey(new Date());
99
+ const today = parseISO(dayKey);
88
100
  for (const occurrence of occurrences) {
89
- for (const day of daysOf(occurrence)) {
101
+ for (const day of daysOf(occurrence, today)) {
90
102
  const key = localDateKey(day);
91
- // The days an event spent before today are not listed.
92
- if (key < todayKey) {
93
- continue;
94
- }
95
103
  const group = byDay.get(key) ?? { day, items: [] };
96
104
  group.items.push(occurrence);
97
105
  byDay.set(key, group);
@@ -100,17 +108,76 @@ export default function ListView({ occurrences, folderColors, onSelectEvent, foc
100
108
  return Array.from(byDay.entries())
101
109
  .sort(([a], [b]) => a.localeCompare(b))
102
110
  .map(([key, { day, items }]) => ({ key, day, items: sortedForDay(items, day) }));
103
- }, [occurrences]);
111
+ }, [occurrences, dayKey]);
112
+
113
+ // How many days are rendered. Where there is no IntersectionObserver to say the end was reached, all of them are.
114
+ const [shown, setShown] = useState(LIST_INITIAL_DAYS);
115
+ const rendered = typeof IntersectionObserver === "undefined" ? groups.length : Math.min(shown, groups.length);
116
+ const more = rendered < groups.length;
117
+ useEffect(() => {
118
+ const sentinel = sentinelRef.current;
119
+ if (!sentinel) {
120
+ return;
121
+ }
122
+ // Watched again after each page, so a sentinel still in view (a tall window) brings the next one.
123
+ const observer = new IntersectionObserver(
124
+ (entries) => {
125
+ if (entries.some((entry) => entry.isIntersecting)) {
126
+ setShown((count) => count + LIST_DAYS_PER_PAGE);
127
+ }
128
+ },
129
+ { root: scrollerRef.current, rootMargin: "600px" },
130
+ );
131
+ observer.observe(sentinel);
132
+ return () => observer.disconnect();
133
+ }, [more, shown]);
134
+
135
+ // What waits for the days it scrolls to be rendered: one slot per effect that asks, so one cannot replace another's scroll.
136
+ const afterReveal = useRef<Partial<Record<"jump" | "match" | "step", () => void>>>({});
137
+ useEffect(() => {
138
+ const pending = Object.values(afterReveal.current);
139
+ afterReveal.current = {};
140
+ for (const scroll of pending) {
141
+ scroll();
142
+ }
143
+ }, [shown]);
144
+ /** Runs `scroll` once the day at `index` of `groups` is rendered: now, or after the rendered days have grown to reach it. */
145
+ function reveal(slot: "jump" | "match" | "step", index: number, scroll: () => void) {
146
+ if (index < rendered) {
147
+ scroll();
148
+ return;
149
+ }
150
+ afterReveal.current[slot] = scroll;
151
+ setShown((count) => Math.max(count, index + LIST_DAYS_PER_PAGE));
152
+ }
153
+ // Both look the element up when the scroll runs, which can be after the list changed (a live refresh) and the element is gone: then there is nothing to scroll to.
154
+ const scrollToDay = (key: string) => scrollerRef.current?.querySelector<HTMLElement>(`[data-day="${key}"]`)?.scrollIntoView({ block: "start" });
104
155
 
105
156
  const dayElements = () => Array.from(scrollerRef.current!.querySelectorAll<HTMLElement>("[data-day]"));
106
157
 
107
158
  // On opening and on a jump (Today, a date picked in the mini calendar): the first day from `focusDate` that has events, else the last one.
108
159
  useEffect(() => {
160
+ if (groups.length === 0) {
161
+ return;
162
+ }
109
163
  const wanted = localDateKey(focusDate);
110
- const days = dayElements();
111
- (days.find((el) => el.dataset.day! >= wanted) ?? days[days.length - 1])?.scrollIntoView({ block: "start" });
164
+ const found = groups.findIndex((group) => group.key >= wanted);
165
+ const index = found >= 0 ? found : groups.length - 1;
166
+ reveal("jump", index, () => scrollToDay(groups[index].key));
112
167
  }, [jumpNonce]);
113
168
 
169
+ // A search match beyond the rendered days: they grow to it, and it is scrolled to (the page's own scroll to the match found nothing there).
170
+ useEffect(() => {
171
+ const index = groups.findIndex((group) => group.items.some((occurrence) => occurrence.occurrenceKey === activeKey));
172
+ if (index >= rendered) {
173
+ reveal("match", index, () =>
174
+ Array.from(scrollerRef.current?.querySelectorAll<HTMLElement>("[data-occurrence-key]") ?? [])
175
+ .find((el) => el.dataset.occurrenceKey === activeKey)
176
+ ?.scrollIntoView({ block: "nearest", inline: "nearest" }),
177
+ );
178
+ }
179
+ }, [activeKey]);
180
+
114
181
  // A step: from the month of the day at the top of the list to the first day of the previous or next month that has any.
115
182
  useEffect(() => {
116
183
  const days = dayElements();
@@ -119,19 +186,22 @@ export default function ListView({ occurrences, folderColors, onSelectEvent, foc
119
186
  }
120
187
  const top = scrollerRef.current!.getBoundingClientRect().top;
121
188
  const current = days.find((el) => el.getBoundingClientRect().bottom > top) ?? days[days.length - 1];
122
- const months = Array.from(new Set(days.map((el) => el.dataset.day!.slice(0, 7))));
189
+ const months = Array.from(new Set(groups.map((group) => group.key.slice(0, 7))));
123
190
  const month = months[months.indexOf(current.dataset.day!.slice(0, 7)) + step.direction];
124
191
  // Past the first or last month with events there is nowhere to go.
125
- days.find((el) => el.dataset.day!.startsWith(month))?.scrollIntoView({ block: "start" });
192
+ const index = groups.findIndex((group) => group.key.startsWith(month));
193
+ if (index >= 0) {
194
+ reveal("step", index, () => scrollToDay(groups[index].key));
195
+ }
126
196
  }, [step]);
127
197
 
128
- const thisYear = new Date().getFullYear();
198
+ const thisYear = parseISO(dayKey).getFullYear();
129
199
  return (
130
200
  <div ref={scrollerRef} role="region" aria-label="List" data-calendar-scroller className="flex-1 min-h-0 overflow-y-auto">
131
201
  {groups.length === 0 ? (
132
202
  <p className="p-4 text-sm text-text-muted">No upcoming events</p>
133
203
  ) : (
134
- groups.map(({ key, day, items }) => (
204
+ groups.slice(0, rendered).map(({ key, day, items }) => (
135
205
  <section key={key} data-day={key} aria-labelledby={`list-day-${key}`}>
136
206
  <h3
137
207
  id={`list-day-${key}`}
@@ -157,18 +227,22 @@ export default function ListView({ occurrences, folderColors, onSelectEvent, foc
157
227
  day={day}
158
228
  color={folderColors[occurrence.folderUid]}
159
229
  onSelect={onSelectEvent}
160
- activeKey={activeKey}
230
+ // Only the row that is the match is told: a new match renders two rows, not all of them.
231
+ activeKey={activeKey === occurrence.occurrenceKey ? activeKey : null}
161
232
  />
162
233
  ))}
163
234
  </ul>
164
235
  </section>
165
236
  ))
166
237
  )}
238
+ {more && <div ref={sentinelRef} data-list-end aria-hidden="true" className="h-px" />}
167
239
  </div>
168
240
  );
169
241
  }
170
242
 
171
- function ListRow({
243
+ export default memo(ListView);
244
+
245
+ const ListRow = memo(function ListRow({
172
246
  occurrence,
173
247
  day,
174
248
  color,
@@ -204,4 +278,4 @@ function ListRow({
204
278
  </button>
205
279
  </li>
206
280
  );
207
- }
281
+ });
@@ -22,6 +22,10 @@ const FREQ_LABEL: Record<RecurrenceFrequency, { unit: string; unitPlural: string
22
22
 
23
23
  type EndCondition = "never" | "count" | "until";
24
24
 
25
+ /** The most an interval ("every N days") or a count ("after N occurrences") can be set to. */
26
+ const MAX_INTERVAL = 999;
27
+ const MAX_COUNT = 1000;
28
+
25
29
  function endConditionOf(rule: RecurrenceRule): EndCondition {
26
30
  if (rule.count) return "count";
27
31
  if (rule.until) return "until";
@@ -38,6 +42,9 @@ export interface RecurrenceEditorProps {
38
42
  /** The weekday the event starts on (in the frame its series expands in - see `allDay.ts`'s
39
43
  * `startWeekdayCode()`): the day a new weekly rule, or one switched to Weekly, repeats on. Defaults to Monday. */
40
44
  startWeekday?: WeekdayCode;
45
+ /** The event's start date (`yyyy-MM-dd`): "Ends on" starts at today, or at this date when the event starts later - a series that ends before it
46
+ * starts has no occurrences at all. */
47
+ startDateKey?: string;
41
48
  /** Leaves out the "Repeats" checkbox, for a caller that already turns repeating on and off itself (the event form's
42
49
  * "Does not repeat" menu): what remains is the rule's details, shown only while there is a rule. */
43
50
  hideToggle?: boolean;
@@ -49,7 +56,7 @@ export interface RecurrenceEditorProps {
49
56
  * live "every ... until/for ..." summary comes from `describeRecurrence()` (built on `rrule`'s own
50
57
  * `.toText()`), so it never drifts out of sync with what will actually be submitted.
51
58
  */
52
- export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO", hideToggle = false }: RecurrenceEditorProps) {
59
+ export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO", startDateKey, hideToggle = false }: RecurrenceEditorProps) {
53
60
  function handleEnable(enabled: boolean) {
54
61
  onChange(enabled ? { freq: "weekly", interval: 1, byDay: [startWeekday], exceptions: [] } : null);
55
62
  }
@@ -83,25 +90,27 @@ export default function RecurrenceEditor({ value, onChange, allDay = false, star
83
90
  } else if (condition === "count") {
84
91
  update({ count: 10, until: undefined });
85
92
  } else {
86
- update({ until: recurrenceUntilInstant(format(new Date(), "yyyy-MM-dd"), allDay), count: undefined });
93
+ const today = format(new Date(), "yyyy-MM-dd");
94
+ update({ until: recurrenceUntilInstant(startDateKey && startDateKey > today ? startDateKey : today, allDay), count: undefined });
87
95
  }
88
96
  }
89
97
 
90
- /** A positive whole number from a number input, or `null` while it's empty (a user clearing the field
91
- * to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN. */
92
- function parsePositive(raw: string): number | null {
98
+ /** A whole number from 1 to `max` from a number input, or `null` while it's empty (a user clearing the field
99
+ * to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN. A huge one (an event
100
+ * every day, a billion times) would make every view expand it forever. */
101
+ function parsePositive(raw: string, max: number): number | null {
93
102
  if (raw.trim() === "") return null;
94
103
  const n = Math.floor(Number(raw));
95
- return n >= 1 ? n : 1;
104
+ return n >= 1 ? Math.min(n, max) : 1;
96
105
  }
97
106
 
98
107
  function handleInterval(raw: string) {
99
- const interval = parsePositive(raw);
108
+ const interval = parsePositive(raw, MAX_INTERVAL);
100
109
  if (interval !== null) update({ interval });
101
110
  }
102
111
 
103
112
  function handleCount(raw: string) {
104
- const count = parsePositive(raw);
113
+ const count = parsePositive(raw, MAX_COUNT);
105
114
  if (count !== null) update({ count });
106
115
  }
107
116
 
@@ -126,6 +135,7 @@ export default function RecurrenceEditor({ value, onChange, allDay = false, star
126
135
  <input
127
136
  type="number"
128
137
  min={1}
138
+ max={MAX_INTERVAL}
129
139
  className={`${INPUT_CLASS} w-16`}
130
140
  value={value.interval}
131
141
  onChange={(e) => handleInterval(e.target.value)}
@@ -189,6 +199,7 @@ export default function RecurrenceEditor({ value, onChange, allDay = false, star
189
199
  <input
190
200
  type="number"
191
201
  min={1}
202
+ max={MAX_COUNT}
192
203
  className={`${INPUT_CLASS} w-16`}
193
204
  value={value.count ?? 10}
194
205
  disabled={endConditionOf(value) !== "count"}
@@ -3,15 +3,13 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React from "react";
6
- import { addDays, addMinutes, format, startOfDay } from "date-fns";
6
+ import { addMinutes, format, startOfDay } from "date-fns";
7
7
  import { CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
8
8
  import { occursOnDay } from "./allDay.js";
9
9
  import { occurrenceMarker, useActiveOccurrenceKey } from "./activeOccurrence.js";
10
10
  import { EventAnchor, anchorOf } from "./EventShell.js";
11
+ import { HOUR_HEIGHT_PX, SLOTS_PER_DAY, SLOT_MINUTES, blockSpan, laneStyle, layoutLanes, slotStart } from "./gridLayout.js";
11
12
 
12
- const HOUR_HEIGHT_PX = 48;
13
- const SLOT_MINUTES = 30;
14
- const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
15
13
  const HOURS = Array.from({ length: 24 }, (_, h) => h);
16
14
 
17
15
  export interface SplitDayColumn {
@@ -93,31 +91,30 @@ function SplitColumn({
93
91
  onSelectSlot: (start: Date, end: Date, folderUid: string, anchor: EventAnchor) => void;
94
92
  }) {
95
93
  const activeKey = useActiveOccurrenceKey();
94
+ // Clipped to this day, so a multi-day event fills only the part of the day it covers. An all-day event is date-only (see `allDay.ts`), so it
95
+ // always spans the whole local day. Events that overlap in time share the column's width instead of covering one another.
96
+ const spans = occurrences.map((occurrence) =>
97
+ blockSpan(new Date(occurrence.startDate).getTime(), new Date(occurrence.endDate).getTime(), dayStart, occurrence.allDay),
98
+ );
99
+ const lanes = layoutLanes(spans);
96
100
  return (
97
101
  <div className="flex-1 min-w-0 relative border-l border-border">
98
102
  {Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
99
- const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
103
+ const start = slotStart(dayStart, i);
100
104
  return (
101
105
  <button
102
106
  key={i}
103
107
  type="button"
104
- onClick={(e) => onSelectSlot(slotStart, addMinutes(slotStart, SLOT_MINUTES), column.folderUid, anchorOf(e.currentTarget))}
108
+ onClick={(e) => onSelectSlot(start, addMinutes(start, SLOT_MINUTES), column.folderUid, anchorOf(e.currentTarget))}
105
109
  style={{ height: HOUR_HEIGHT_PX / 2 }}
106
110
  className="block w-full border-b border-border/50 text-left"
107
- aria-label={`New event at ${format(slotStart, "h:mm a")} in ${column.name}`}
111
+ aria-label={`New event at ${format(start, "h:mm a")} in ${column.name}`}
108
112
  />
109
113
  );
110
114
  })}
111
- {occurrences.map((occurrence) => {
115
+ {occurrences.map((occurrence, index) => {
112
116
  const start = new Date(occurrence.startDate);
113
- const end = new Date(occurrence.endDate);
114
- // Clipped to this day, so a multi-day event fills only the part of the day it covers. An
115
- // all-day event is date-only (see `allDay.ts`), so it always spans the whole local day.
116
- const dayEnd = addDays(dayStart, 1).getTime();
117
- const visibleStart = occurrence.allDay ? dayStart.getTime() : Math.max(start.getTime(), dayStart.getTime());
118
- const visibleEnd = occurrence.allDay ? dayEnd : Math.min(end.getTime(), dayEnd);
119
- const top = ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX;
120
- const height = Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16);
117
+ const { top, height } = spans[index];
121
118
  const isFree = occurrence.busyStatus === "free";
122
119
  const marker = occurrenceMarker(activeKey, occurrence);
123
120
  return (
@@ -129,8 +126,7 @@ function SplitColumn({
129
126
  position: "absolute",
130
127
  top,
131
128
  height,
132
- left: 2,
133
- right: 2,
129
+ ...laneStyle(lanes[index]),
134
130
  ...(isFree ? undefined : { backgroundColor: column.color, color: "#fff" }),
135
131
  }}
136
132
  className={[
@@ -20,6 +20,7 @@ import { TaskList, TaskPriority, createTask, listTaskLists } from "../../../../l
20
20
  import Button from "../../../../lib/components/buttons/Button.js";
21
21
  import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
22
22
  import { notify } from "../../notifications/store.js";
23
+ import { dueDateInstant } from "../tasks/dueDate.js";
23
24
  import { EventFormController } from "./eventForm.js";
24
25
  import { INPUT_CLASS, IconRow, SELECT_CLASS } from "./EventFormParts.js";
25
26
  import QuickFaceFrame from "./QuickFaceFrame.js";
@@ -127,8 +128,8 @@ export default function TaskQuickForm({ c, draft, onDraftChange, tabs, expanded,
127
128
  folderUid,
128
129
  title,
129
130
  body: draft.body.trim() || undefined,
130
- // A date-only due date is the local day's midnight, the way the Tasks app writes one.
131
- dueDate: draft.dueDate ? parseISO(draft.allDay ? draft.dueDate : `${draft.dueDate}T${draft.dueTime}`).toISOString() : undefined,
131
+ // A date-only due date is that day's UTC midnight, the way the Tasks app writes one.
132
+ dueDate: draft.dueDate ? (draft.allDay ? dueDateInstant(draft.dueDate) : parseISO(`${draft.dueDate}T${draft.dueTime}`).toISOString()) : undefined,
132
133
  priority: draft.priority,
133
134
  reminderDate: draft.reminder ? parseISO(draft.reminder).toISOString() : undefined,
134
135
  taskListUid: listUid || undefined,
@@ -2,18 +2,16 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React from "react";
5
+ import React, { KeyboardEvent } from "react";
6
6
  import { useDraggable, useDroppable } from "@dnd-kit/core";
7
- import { addDays, addMinutes, format, isToday, startOfDay } from "date-fns";
7
+ import { addMinutes, format, isToday, startOfDay } from "date-fns";
8
8
  import { dayDropId, eventDragId, resizeDragId, slotDropId } from "../../../../lib/calendar/calendarDragIds.js";
9
9
  import { CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
10
10
  import { occursOnDay, startsOnDay } from "./allDay.js";
11
11
  import { occurrenceMarker, useActiveOccurrenceKey } from "./activeOccurrence.js";
12
12
  import { EventAnchor, anchorOf } from "./EventShell.js";
13
+ import { HOUR_HEIGHT_PX, Lane, SLOTS_PER_DAY, SLOT_MINUTES, blockSpan, laneStyle, layoutLanes, slotStart } from "./gridLayout.js";
13
14
 
14
- const HOUR_HEIGHT_PX = 48;
15
- const SLOT_MINUTES = 30;
16
- const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
17
15
  const HOURS = Array.from({ length: 24 }, (_, h) => h);
18
16
 
19
17
  export interface TimeGridViewProps {
@@ -111,20 +109,22 @@ interface DayColumnProps {
111
109
 
112
110
  function DayColumn({ day, occurrences, folderColors, onSelectEvent, onSelectSlot }: DayColumnProps) {
113
111
  const dayStart = startOfDay(day);
112
+ // Events that overlap in time share the column's width instead of covering one another.
113
+ const lanes = layoutLanes(occurrences.map((occurrence) => blockSpan(new Date(occurrence.startDate).getTime(), new Date(occurrence.endDate).getTime(), dayStart)));
114
114
 
115
115
  return (
116
116
  <div className={["flex-1 min-w-0 relative border-l border-border", isToday(day) ? "bg-primary/5" : ""].join(" ")}>
117
- {Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
118
- const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
119
- return <TimeSlot key={i} start={slotStart} onSelectSlot={onSelectSlot} />;
120
- })}
121
- {occurrences.map((occurrence) =>
117
+ {Array.from({ length: SLOTS_PER_DAY }, (_, i) => (
118
+ <TimeSlot key={i} start={slotStart(dayStart, i)} onSelectSlot={onSelectSlot} />
119
+ ))}
120
+ {occurrences.map((occurrence, index) =>
122
121
  startsOnDay(occurrence, day) ? (
123
122
  <EventBlock
124
123
  key={occurrence.occurrenceKey}
125
124
  occurrence={occurrence}
126
125
  color={folderColors[occurrence.folderUid]}
127
126
  dayStart={dayStart}
127
+ lane={lanes[index]}
128
128
  onSelect={onSelectEvent}
129
129
  />
130
130
  ) : (
@@ -133,6 +133,7 @@ function DayColumn({ day, occurrences, folderColors, onSelectEvent, onSelectSlot
133
133
  occurrence={occurrence}
134
134
  color={folderColors[occurrence.folderUid]}
135
135
  dayStart={dayStart}
136
+ lane={lanes[index]}
136
137
  onSelect={onSelectEvent}
137
138
  />
138
139
  ),
@@ -147,6 +148,8 @@ function TimeSlot({ start, onSelectSlot }: { start: Date; onSelectSlot: (start:
147
148
  <button
148
149
  ref={setNodeRef}
149
150
  type="button"
151
+ // 48 of these a day would be hundreds of tab stops in a week: the New event shortcut is the keyboard's way in.
152
+ tabIndex={-1}
150
153
  onClick={(e) => onSelectSlot(start, addMinutes(start, SLOT_MINUTES), anchorOf(e.currentTarget))}
151
154
  style={{ height: HOUR_HEIGHT_PX / 2 }}
152
155
  className={["block w-full border-b border-border/50 text-left", isOver ? "bg-primary/10" : ""].join(" ")}
@@ -155,15 +158,18 @@ function TimeSlot({ start, onSelectSlot }: { start: Date; onSelectSlot: (start:
155
158
  );
156
159
  }
157
160
 
161
+ /** Enter or Space on a focused event block opens the event, as a click does (the blocks are not `<button>`s: they hold a nested resize handle). */
162
+ function openOnKey(event: KeyboardEvent<HTMLElement>, open: () => void) {
163
+ if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) {
164
+ event.preventDefault();
165
+ open();
166
+ }
167
+ }
168
+
158
169
  /** A block's position within its day column. A multi-day event is drawn in every day column it
159
170
  * overlaps, clipped to that day. */
160
171
  function blockGeometry(occurrence: CalendarOccurrence, dayStart: Date): { top: number; height: number } {
161
- const visibleStart = Math.max(new Date(occurrence.startDate).getTime(), dayStart.getTime());
162
- const visibleEnd = Math.min(new Date(occurrence.endDate).getTime(), addDays(dayStart, 1).getTime());
163
- return {
164
- top: ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX,
165
- height: Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16),
166
- };
172
+ return blockSpan(new Date(occurrence.startDate).getTime(), new Date(occurrence.endDate).getTime(), dayStart);
167
173
  }
168
174
 
169
175
  /** A multi-day event's block in a day column after its first: clickable, but not draggable/resizable
@@ -172,11 +178,13 @@ function ContinuationBlock({
172
178
  occurrence,
173
179
  color,
174
180
  dayStart,
181
+ lane,
175
182
  onSelect,
176
183
  }: {
177
184
  occurrence: CalendarOccurrence;
178
185
  color: string;
179
186
  dayStart: Date;
187
+ lane: Lane;
180
188
  onSelect: (occurrence: CalendarOccurrence) => void;
181
189
  }) {
182
190
  const isFree = occurrence.busyStatus === "free";
@@ -185,8 +193,11 @@ function ContinuationBlock({
185
193
  return (
186
194
  <div
187
195
  {...marker.attrs}
196
+ role="button"
197
+ tabIndex={0}
188
198
  onClick={() => onSelect(occurrence)}
189
- style={{ position: "absolute", top, height, left: 2, right: 2, ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }) }}
199
+ onKeyDown={(e) => openOnKey(e, () => onSelect(occurrence))}
200
+ style={{ position: "absolute", top, height, ...laneStyle(lane), ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }) }}
190
201
  className={["rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer", isFree ? "bg-surface-alt text-text-muted" : "", marker.className].join(" ")}
191
202
  >
192
203
  <div className="font-medium truncate">{occurrence.title}</div>
@@ -198,6 +209,7 @@ function EventBlock({
198
209
  occurrence,
199
210
  color,
200
211
  dayStart,
212
+ lane,
201
213
  onSelect,
202
214
  }: {
203
215
  occurrence: CalendarOccurrence;
@@ -205,6 +217,8 @@ function EventBlock({
205
217
  * "free" one keeps Outlook's own muted/outline treatment regardless of which calendar it's on. */
206
218
  color: string;
207
219
  dayStart: Date;
220
+ /** Its share of the column's width, when other events overlap it in time. */
221
+ lane: Lane;
208
222
  onSelect: (occurrence: CalendarOccurrence) => void;
209
223
  }) {
210
224
  const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ id: eventDragId(occurrence) });
@@ -230,8 +244,7 @@ function EventBlock({
230
244
  position: "absolute",
231
245
  top,
232
246
  height,
233
- left: 2,
234
- right: 2,
247
+ ...laneStyle(lane),
235
248
  transform: transform ? `translate(${transform.x}px, ${transform.y}px)` : undefined,
236
249
  zIndex: isDragging ? 10 : undefined,
237
250
  ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }),
@@ -244,6 +257,7 @@ function EventBlock({
244
257
  ].join(" ")}
245
258
  {...listeners}
246
259
  {...attributes}
260
+ onKeyDown={(e) => openOnKey(e, () => onSelect(occurrence))}
247
261
  >
248
262
  <div className="font-medium truncate">{occurrence.title}</div>
249
263
  <div className="truncate">{format(start, "h:mma")}</div>
@@ -253,6 +267,8 @@ function EventBlock({
253
267
  aria-label={`Resize "${occurrence.title}"`}
254
268
  {...resizeListeners}
255
269
  {...resizeAttributes}
270
+ // Dragging is the only way to resize: a tab stop here would do nothing.
271
+ tabIndex={-1}
256
272
  />
257
273
  </div>
258
274
  );