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