@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.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +21 -2
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
111
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
* to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN.
|
|
92
|
-
|
|
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 {
|
|
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
|
|
103
|
+
const start = slotStart(dayStart, i);
|
|
100
104
|
return (
|
|
101
105
|
<button
|
|
102
106
|
key={i}
|
|
103
107
|
type="button"
|
|
104
|
-
onClick={(e) => onSelectSlot(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
131
|
-
dueDate: draft.dueDate ?
|
|
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 {
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|