@rapidmx/web-client 0.30.0 → 0.32.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 +3 -3
- 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/diagnostics/diagnosticsApi.ts +2 -2
- 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 +195 -46
- 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 +2 -2
- 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/diagnostics/diagnosticsApi.d.ts +2 -2
- 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 +103 -18
- 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 +31 -2
- package/dist/lib/admin/pluginsApi.js +23 -1
- 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 +45 -3
- 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
|
@@ -35,10 +35,31 @@ const STYLE_HAZARDS = [
|
|
|
35
35
|
[/behavior\s*:/gi, "x-behavior:"],
|
|
36
36
|
[/@import[^;]*;?/gi, ""],
|
|
37
37
|
];
|
|
38
|
-
|
|
39
|
-
export function sanitizeCss(css) {
|
|
38
|
+
function neutraliseHazards(css) {
|
|
40
39
|
return STYLE_HAZARDS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), css);
|
|
41
40
|
}
|
|
41
|
+
/** `css` as a browser reads it: comments gone, and every escape resolved - repeatedly, since a decoded backslash can start another (`\5c 76h`). */
|
|
42
|
+
function readAsBrowser(css) {
|
|
43
|
+
let text = css;
|
|
44
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
45
|
+
const next = unescapeCss(text.replace(/\/\*[\s\S]*?\*\//g, " "));
|
|
46
|
+
if (next === text) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
text = next;
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* CSS with the constructs above neutralised. The patterns run on what a browser would read, not only on the raw text: `position:\66ixed` and
|
|
55
|
+
* `min-height:200\76h` are `position:fixed` and `200vh` to it, and a comment inside a name (`position/**\/:fixed`) is no obstacle either. When the
|
|
56
|
+
* decoded text holds one, that text (escapes resolved, comments dropped) is what is kept, neutralised; CSS whose escapes hide nothing is left as written.
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeCss(css) {
|
|
59
|
+
const decoded = readAsBrowser(css);
|
|
60
|
+
const decodedClean = neutraliseHazards(decoded);
|
|
61
|
+
return decodedClean !== decoded ? decodedClean : neutraliseHazards(css);
|
|
62
|
+
}
|
|
42
63
|
/** Elements removed together with everything inside them. */
|
|
43
64
|
const REMOVED_ELEMENTS = new Set([
|
|
44
65
|
"script",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { useEffect, useMemo, useState } from "react";
|
|
21
21
|
import { getMyMailboxAccess } from "../../../../lib/mail/mailboxAccessApi.js";
|
|
22
22
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
23
|
+
import { onSigningOut } from "./compose/composeFlushRegistry.js";
|
|
23
24
|
const inFlight = new Map();
|
|
24
25
|
const settled = new Map();
|
|
25
26
|
/** Forgets every cached answer - for tests, and anything that changes the caller's access mid-session. */
|
|
@@ -27,6 +28,7 @@ export function clearMailboxWritabilityCache() {
|
|
|
27
28
|
inFlight.clear();
|
|
28
29
|
settled.clear();
|
|
29
30
|
}
|
|
31
|
+
onSigningOut(clearMailboxWritabilityCache);
|
|
30
32
|
function isImplicitlyWritable(mailbox, userUid, trusted) {
|
|
31
33
|
return !!trusted || (!!userUid && mailbox.ownerUserUid === userUid);
|
|
32
34
|
}
|
|
@@ -36,6 +36,8 @@ export function hasConditions(conditions) {
|
|
|
36
36
|
export default function RuleBuilder({ value, onChange, conditionFields, actionTypes, showValidation = false, }) {
|
|
37
37
|
const [draftText, setDraftText] = useState({});
|
|
38
38
|
const [newActionType, setNewActionType] = useState(actionTypes[0]?.value ?? "");
|
|
39
|
+
// The sequence field's text while it is being edited (it may be empty, which is no number); `null` shows the rule's own.
|
|
40
|
+
const [sequenceDraft, setSequenceDraft] = useState(null);
|
|
39
41
|
const conditions = value.conditions;
|
|
40
42
|
function patch(next) {
|
|
41
43
|
onChange({ ...value, ...next });
|
|
@@ -102,5 +104,11 @@ export default function RuleBuilder({ value, onChange, conditionFields, actionTy
|
|
|
102
104
|
}) })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-4", children: "Actions" }), _jsxs("div", { className: "flex flex-col gap-3 mb-4", children: [value.actions.length === 0 && _jsx("p", { className: "text-sm text-text-muted", children: "No actions yet." }), value.actions.map((action, index) => {
|
|
103
105
|
const def = actionTypes.find((t) => t.value === action.type);
|
|
104
106
|
return (_jsxs("div", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("span", { className: "text-sm font-semibold", children: def?.label ?? action.type }), _jsx("button", { type: "button", onClick: () => removeAction(index), className: "text-sm text-danger hover:underline", children: "Remove" })] }), def?.render(action, (next) => updateAction(index, next))] }, index));
|
|
105
|
-
})] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("select", { "aria-label": "New action type", className: INPUT_CLASS, value: newActionType, onChange: (e) => setNewActionType(e.target.value), children: actionTypes.map((t) => (_jsx("option", { value: t.value, children: t.label }, t.value))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: addAction, children: "Add action" })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-4", children: "Settings" }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.enabled, onChange: (e) => patch({ enabled: e.target.checked }) }), "Enabled"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.stopProcessingRules, onChange: (e) => patch({ stopProcessingRules: e.target.checked }) }), "Stop processing more rules once this one matches"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: ["Sequence (evaluation order, ascending)", _jsx("input", { type: "number", className: "w-20 text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary", value: value.sequence, onChange: (e) =>
|
|
107
|
+
})] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("select", { "aria-label": "New action type", className: INPUT_CLASS, value: newActionType, onChange: (e) => setNewActionType(e.target.value), children: actionTypes.map((t) => (_jsx("option", { value: t.value, children: t.label }, t.value))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: addAction, children: "Add action" })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-4", children: "Settings" }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.enabled, onChange: (e) => patch({ enabled: e.target.checked }) }), "Enabled"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.stopProcessingRules, onChange: (e) => patch({ stopProcessingRules: e.target.checked }) }), "Stop processing more rules once this one matches"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: ["Sequence (evaluation order, ascending)", _jsx("input", { type: "number", className: "w-20 text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary", value: sequenceDraft ?? value.sequence, onChange: (e) => {
|
|
108
|
+
// What is typed is kept as typed, so the field can be cleared and typed afresh; the rule only takes a number.
|
|
109
|
+
setSequenceDraft(e.target.value);
|
|
110
|
+
if (e.target.value !== "") {
|
|
111
|
+
patch({ sequence: Number(e.target.value) });
|
|
112
|
+
}
|
|
113
|
+
}, onBlur: () => setSequenceDraft(null) })] })] })] })] }));
|
|
106
114
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The opt-in for Gravatar contact pictures. A per-browser preference (not the mailbox's), off until turned on, because looking a picture up tells
|
|
4
|
+
* gravatar.com the contact's address hash and the reader's IP address.
|
|
5
|
+
*/
|
|
6
|
+
export default function GravatarSetting(): React.JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useSyncExternalStore } from "react";
|
|
7
|
+
import { gravatarEnabled, setGravatarEnabled, subscribeGravatarPreference } from "../../../../lib/contacts/gravatar.js";
|
|
8
|
+
/**
|
|
9
|
+
* The opt-in for Gravatar contact pictures. A per-browser preference (not the mailbox's), off until turned on, because looking a picture up tells
|
|
10
|
+
* gravatar.com the contact's address hash and the reader's IP address.
|
|
11
|
+
*/
|
|
12
|
+
export default function GravatarSetting() {
|
|
13
|
+
const enabled = useSyncExternalStore(subscribeGravatarPreference, gravatarEnabled, () => false);
|
|
14
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Contact pictures" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: enabled, onChange: (e) => setGravatarEnabled(e.target.checked) }), "Show profile pictures from Gravatar"] }), _jsx("p", { className: "text-xs text-text-muted mt-2", children: "Looks up each contact's picture by sending a hash of their email address to gravatar.com, which also sees your IP address. Off by default; applies to this browser only." })] }));
|
|
15
|
+
}
|
|
@@ -93,7 +93,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
93
93
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
94
94
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
95
95
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
96
|
-
return _jsx(MailboxProvisioning, {});
|
|
96
|
+
return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
|
|
97
97
|
}
|
|
98
98
|
let inner = null;
|
|
99
99
|
if (userUid && status === "checking") {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stored value of a date-only due date (a `yyyy-MM-dd` day from a date input): UTC midnight of that day, so the task is due on the same
|
|
3
|
+
* calendar day for everyone, whatever zone they read it in - the way an all-day event is stored. (The creator's local midnight would be a
|
|
4
|
+
* different day for a reader in another zone.)
|
|
5
|
+
*/
|
|
6
|
+
export declare function dueDateInstant(dateKey: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* A task's `dueDate` as a local `Date`. An exact UTC midnight is a date-only due date (see `dueDateInstant()`) and reads as the start of that
|
|
9
|
+
* calendar day wherever the reader is; any other instant - a time on the day, or a date-only value an older version stored as the creator's local
|
|
10
|
+
* midnight - is that instant itself. (`parseISO()` rather than `new Date()`: a bare `yyyy-MM-dd` would be read as UTC.)
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseDueDate(value: string): Date;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { parseISO } from "date-fns";
|
|
6
|
+
const MS_PER_DAY = 86400000;
|
|
7
|
+
/**
|
|
8
|
+
* The stored value of a date-only due date (a `yyyy-MM-dd` day from a date input): UTC midnight of that day, so the task is due on the same
|
|
9
|
+
* calendar day for everyone, whatever zone they read it in - the way an all-day event is stored. (The creator's local midnight would be a
|
|
10
|
+
* different day for a reader in another zone.)
|
|
11
|
+
*/
|
|
12
|
+
export function dueDateInstant(dateKey) {
|
|
13
|
+
return `${dateKey}T00:00:00.000Z`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A task's `dueDate` as a local `Date`. An exact UTC midnight is a date-only due date (see `dueDateInstant()`) and reads as the start of that
|
|
17
|
+
* calendar day wherever the reader is; any other instant - a time on the day, or a date-only value an older version stored as the creator's local
|
|
18
|
+
* midnight - is that instant itself. (`parseISO()` rather than `new Date()`: a bare `yyyy-MM-dd` would be read as UTC.)
|
|
19
|
+
*/
|
|
20
|
+
export function parseDueDate(value) {
|
|
21
|
+
const instant = parseISO(value);
|
|
22
|
+
if (instant.getTime() % MS_PER_DAY !== 0) {
|
|
23
|
+
return instant;
|
|
24
|
+
}
|
|
25
|
+
return new Date(instant.getUTCFullYear(), instant.getUTCMonth(), instant.getUTCDate());
|
|
26
|
+
}
|
|
@@ -79,7 +79,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
79
79
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
80
80
|
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
81
81
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
82
|
-
return _jsx(MailboxProvisioning, {});
|
|
82
|
+
return _jsx(MailboxProvisioning, { impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl });
|
|
83
83
|
}
|
|
84
84
|
let inner = null;
|
|
85
85
|
if (userUid && status === "checking") {
|
|
@@ -9,6 +9,14 @@ export interface DecryptedMessage {
|
|
|
9
9
|
subject?: string;
|
|
10
10
|
preview?: string;
|
|
11
11
|
}
|
|
12
|
+
/** Whether decrypting this message already came to nothing since the keys were last unlocked, recently enough that trying again would only repeat it. */
|
|
13
|
+
export declare function decryptFailedBefore(uid: string): boolean;
|
|
14
|
+
/** Remembers that decrypting this message came to nothing, so a list that refreshes does not download and try it again each time. */
|
|
15
|
+
export declare function markDecryptFailed(uid: string): void;
|
|
16
|
+
/** Forgets everything remembered about messages that could not be decrypted (sign-out, an account switch). */
|
|
17
|
+
export declare function clearDecryptedMessages(): void;
|
|
18
|
+
/** Runs `task` over `items` with at most `limit` running at once, resolving to the results in the order of `items`. */
|
|
19
|
+
export declare function mapWithConcurrency<T, R>(items: T[], limit: number, task: (item: T) => Promise<R>): Promise<R[]>;
|
|
12
20
|
/** Plain text of decrypted HTML for a one-line preview. Only ever feeds text display, never markup. */
|
|
13
21
|
export declare function previewOf(html: string): string;
|
|
14
22
|
/** Remembers what was recovered of one message. Ignored when there is nothing new to show. */
|
|
@@ -5,12 +5,52 @@
|
|
|
5
5
|
import { useSyncExternalStore } from "react";
|
|
6
6
|
import { getUnlockedKeys, subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
7
7
|
import { getMessageRawContent } from "../../../lib/mail/mailApi.js";
|
|
8
|
+
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
8
9
|
const PREVIEW_MAX_LENGTH = 160;
|
|
9
10
|
let entries = {};
|
|
10
11
|
/** Bumped whenever a mailbox's keys are unlocked, so a view can retry decrypting what it couldn't before. */
|
|
11
12
|
let unlockEpoch = 0;
|
|
12
13
|
const listeners = new Set();
|
|
13
14
|
const inFlight = new Set();
|
|
15
|
+
/** How long a message that could not be decrypted is left alone before it is tried again (it is also tried again after any unlock). */
|
|
16
|
+
const DECRYPT_RETRY_MS = 10 * 60000;
|
|
17
|
+
/** How many such messages are remembered. */
|
|
18
|
+
const MAX_FAILED = 1000;
|
|
19
|
+
/** The messages whose decrypting came to nothing (fetch failed, not for this key, nothing readable), by uid: when and in which unlock epoch. */
|
|
20
|
+
const failed = new Map();
|
|
21
|
+
/** Whether decrypting this message already came to nothing since the keys were last unlocked, recently enough that trying again would only repeat it. */
|
|
22
|
+
export function decryptFailedBefore(uid) {
|
|
23
|
+
const known = failed.get(uid);
|
|
24
|
+
return !!known && known.epoch === unlockEpoch && Date.now() - known.at < DECRYPT_RETRY_MS;
|
|
25
|
+
}
|
|
26
|
+
/** Remembers that decrypting this message came to nothing, so a list that refreshes does not download and try it again each time. */
|
|
27
|
+
export function markDecryptFailed(uid) {
|
|
28
|
+
failed.delete(uid);
|
|
29
|
+
if (failed.size >= MAX_FAILED) {
|
|
30
|
+
failed.delete(failed.keys().next().value);
|
|
31
|
+
}
|
|
32
|
+
failed.set(uid, { epoch: unlockEpoch, at: Date.now() });
|
|
33
|
+
}
|
|
34
|
+
/** Forgets everything remembered about messages that could not be decrypted (sign-out, an account switch). */
|
|
35
|
+
export function clearDecryptedMessages() {
|
|
36
|
+
entries = {};
|
|
37
|
+
failed.clear();
|
|
38
|
+
emit();
|
|
39
|
+
}
|
|
40
|
+
onSigningOut(clearDecryptedMessages);
|
|
41
|
+
/** Runs `task` over `items` with at most `limit` running at once, resolving to the results in the order of `items`. */
|
|
42
|
+
export async function mapWithConcurrency(items, limit, task) {
|
|
43
|
+
const results = new Array(items.length);
|
|
44
|
+
let next = 0;
|
|
45
|
+
async function worker() {
|
|
46
|
+
while (next < items.length) {
|
|
47
|
+
const index = next++;
|
|
48
|
+
results[index] = await task(items[index]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
14
54
|
function emit() {
|
|
15
55
|
for (const listener of [...listeners]) {
|
|
16
56
|
listener();
|
|
@@ -75,7 +115,7 @@ export function useUnlockEpoch() {
|
|
|
75
115
|
*/
|
|
76
116
|
export async function decryptForDisplay(uid, mailboxUid) {
|
|
77
117
|
const unlocked = getUnlockedKeys(mailboxUid);
|
|
78
|
-
if (!unlocked || entries[uid] || inFlight.has(uid)) {
|
|
118
|
+
if (!unlocked || entries[uid] || inFlight.has(uid) || decryptFailedBefore(uid)) {
|
|
79
119
|
return;
|
|
80
120
|
}
|
|
81
121
|
inFlight.add(uid);
|
|
@@ -86,10 +126,14 @@ export async function decryptForDisplay(uid, mailboxUid) {
|
|
|
86
126
|
// A lock while this ran: the result must not go back on screen.
|
|
87
127
|
if (getUnlockedKeys(mailboxUid) === unlocked) {
|
|
88
128
|
rememberDecrypted(uid, mailboxUid, { subject: security.subject, preview: security.html ? previewOf(security.html) : security.text?.slice(0, PREVIEW_MAX_LENGTH) });
|
|
129
|
+
if (!security.subject && !security.html && !security.text) {
|
|
130
|
+
markDecryptFailed(uid);
|
|
131
|
+
}
|
|
89
132
|
}
|
|
90
133
|
}
|
|
91
134
|
catch {
|
|
92
|
-
// Left as the placeholder.
|
|
135
|
+
// Left as the placeholder - and not fetched again until the next unlock.
|
|
136
|
+
markDecryptFailed(uid);
|
|
93
137
|
}
|
|
94
138
|
finally {
|
|
95
139
|
inFlight.delete(uid);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
1
2
|
/** How long a snapshot may be shown after it was last written. Longer than that the folder is shown as loading, as before. */
|
|
2
3
|
export const LIST_SNAPSHOT_TTL_MS = 5 * 60000;
|
|
3
4
|
/** How many folders are remembered; the least recently written is dropped first. */
|
|
@@ -41,3 +42,4 @@ export function saveListScroll(key, scrollTop) {
|
|
|
41
42
|
export function clearListSnapshots() {
|
|
42
43
|
snapshots.clear();
|
|
43
44
|
}
|
|
45
|
+
onSigningOut(clearListSnapshots);
|
|
@@ -73,8 +73,11 @@ export interface SendDecisionInput {
|
|
|
73
73
|
* can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
|
|
74
74
|
* *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
|
|
75
75
|
*
|
|
76
|
-
* Encryption follows `evaluateEncryptionRequirement()`
|
|
77
|
-
* encrypts, is encrypted; everything unknown is plain
|
|
78
|
-
*
|
|
76
|
+
* Encryption follows `evaluateEncryptionRequirement()`. The compose window's early check (`lookupsComplete: false`) is fail open: only a message
|
|
77
|
+
* the user asked to encrypt, or one the loaded policy encrypts, is encrypted there; everything unknown is plain, so a flapping gateway never
|
|
78
|
+
* blocks a draft or Close. The background send (`lookupsComplete: true`) is not: once its loads are in, a message that might have to be encrypted
|
|
79
|
+
* because the policy or a recipient's key could not be checked (the sender having a key to encrypt with) is blocked (`lookup-failed`) rather than
|
|
80
|
+
* sent in the clear - and so is one the user *explicitly* asked to encrypt whose recipients' keys could not be looked up. Either way the sender can
|
|
81
|
+
* choose to send it without encryption.
|
|
79
82
|
*/
|
|
80
83
|
export declare function decideSend(input: SendDecisionInput): SendDecision;
|
|
@@ -25,9 +25,12 @@ export const INLINE_IMAGES_UNSUPPORTED_MESSAGE = "A signed or encrypted message
|
|
|
25
25
|
* can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
|
|
26
26
|
* *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
|
|
27
27
|
*
|
|
28
|
-
* Encryption follows `evaluateEncryptionRequirement()`
|
|
29
|
-
* encrypts, is encrypted; everything unknown is plain
|
|
30
|
-
*
|
|
28
|
+
* Encryption follows `evaluateEncryptionRequirement()`. The compose window's early check (`lookupsComplete: false`) is fail open: only a message
|
|
29
|
+
* the user asked to encrypt, or one the loaded policy encrypts, is encrypted there; everything unknown is plain, so a flapping gateway never
|
|
30
|
+
* blocks a draft or Close. The background send (`lookupsComplete: true`) is not: once its loads are in, a message that might have to be encrypted
|
|
31
|
+
* because the policy or a recipient's key could not be checked (the sender having a key to encrypt with) is blocked (`lookup-failed`) rather than
|
|
32
|
+
* sent in the clear - and so is one the user *explicitly* asked to encrypt whose recipients' keys could not be looked up. Either way the sender can
|
|
33
|
+
* choose to send it without encryption.
|
|
31
34
|
*/
|
|
32
35
|
export function decideSend(input) {
|
|
33
36
|
const { keys } = input;
|
|
@@ -60,6 +63,13 @@ export function decideSend(input) {
|
|
|
60
63
|
recipients: input.recipients,
|
|
61
64
|
});
|
|
62
65
|
const encrypting = requirement.required;
|
|
66
|
+
if (!encrypting && input.lookupsComplete && requirement.unknown.some((fact) => fact !== "mailbox")) {
|
|
67
|
+
// The sender has a key to encrypt with, the loads are all in (or gave up), and the policy or a recipient's key is still unknown: the message
|
|
68
|
+
// might have to be encrypted, and plaintext must not go out on a guess. The sender can still choose to send it without encryption. (An unknown
|
|
69
|
+
// *mailbox* is not one of these: with no key known there is nothing to say encryption applies, and a mailbox that will not load must not
|
|
70
|
+
// stop everyone's mail.)
|
|
71
|
+
return { action: "blocked", block: { kind: "lookup-failed", message: LOOKUP_UNAVAILABLE_MESSAGE, overrideLabel: "Send without encryption", keysLocked: false } };
|
|
72
|
+
}
|
|
63
73
|
if (encrypting) {
|
|
64
74
|
const statuses = input.recipients.filter((status) => !!status);
|
|
65
75
|
const allResolved = statuses.length === input.recipients.length;
|
|
@@ -45,6 +45,7 @@ export declare const CONTEXT_GRACE_MS = 4000;
|
|
|
45
45
|
export declare function registerOutboxCounter(tracker: (mailboxUid: string) => OutboxCountTracker, onQueued: (mailboxUid: string) => void): () => void;
|
|
46
46
|
/** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server reports later. */
|
|
47
47
|
export declare function retainedRequest(draftUid: string): SendRequest | undefined;
|
|
48
|
+
/** Drops the request a message was queued from, and the pictures remembered as attached to its draft: it has gone, so neither is wanted again. */
|
|
48
49
|
export declare function forgetRetainedRequest(draftUid: string): void;
|
|
49
50
|
/** The sender's signing/encryption key state right now, read from the tab's key session (they can lock mid-send). */
|
|
50
51
|
export declare function readSendKeys(mailboxUid: string, signEnabled: boolean, mailbox: Mailbox | undefined): {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
6
|
-
import { assembleDraft, assembleDraftRaw, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "../../../../lib/mail/mailApi.js";
|
|
6
|
+
import { assembleDraft, assembleDraftRaw, forgetDraftImages, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "../../../../lib/mail/mailApi.js";
|
|
7
7
|
import { describeSendFailure } from "../../../../lib/mail/sendFailure.js";
|
|
8
8
|
import { resolveRecipientEncryption } from "../../../../lib/crypto/composeSecurity.js";
|
|
9
9
|
import { getUnlockedKeys } from "../../../../lib/crypto/keySession.js";
|
|
@@ -33,8 +33,10 @@ const RETAINED_LIMIT = 25;
|
|
|
33
33
|
export function retainedRequest(draftUid) {
|
|
34
34
|
return sendState.retained.get(draftUid);
|
|
35
35
|
}
|
|
36
|
+
/** Drops the request a message was queued from, and the pictures remembered as attached to its draft: it has gone, so neither is wanted again. */
|
|
36
37
|
export function forgetRetainedRequest(draftUid) {
|
|
37
38
|
sendState.retained.delete(draftUid);
|
|
39
|
+
forgetDraftImages(draftUid);
|
|
38
40
|
}
|
|
39
41
|
function retain(request) {
|
|
40
42
|
sendState.retained.delete(request.draft.uid);
|
|
@@ -310,9 +312,11 @@ export function startSend(request, client) {
|
|
|
310
312
|
else if (outcome.kind === "sent") {
|
|
311
313
|
// A server with no queue relayed it before answering: the Sent Items copy exists already.
|
|
312
314
|
markOutgoingSent(uid);
|
|
315
|
+
forgetDraftImages(uid);
|
|
313
316
|
notifySent();
|
|
314
317
|
}
|
|
315
318
|
else {
|
|
319
|
+
forgetDraftImages(uid);
|
|
316
320
|
notify({
|
|
317
321
|
id: `scheduled:${uid}`,
|
|
318
322
|
kind: "success",
|
|
@@ -8,6 +8,9 @@ import { FolderChange } from "./folderTree.js";
|
|
|
8
8
|
export declare const LIVE_POLL_INTERVAL_MS = 45000;
|
|
9
9
|
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
10
10
|
export declare const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
11
|
+
/** Refreshes are also at least this far apart: while mail keeps arriving (a bulk delivery, an import) the list, the folder counts and an open
|
|
12
|
+
* thread are read about once in this time - a trailing refresh then picks up whatever came since - instead of every debounce window. */
|
|
13
|
+
export declare const LIVE_EVENT_MIN_INTERVAL_MS = 1500;
|
|
11
14
|
/** What changed, as far as a list on screen is concerned. */
|
|
12
15
|
export interface LiveUpdates {
|
|
13
16
|
/** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
|
|
@@ -13,6 +13,9 @@ import { sortFolders } from "./folderTree.js";
|
|
|
13
13
|
export const LIVE_POLL_INTERVAL_MS = 45000;
|
|
14
14
|
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
15
15
|
export const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
16
|
+
/** Refreshes are also at least this far apart: while mail keeps arriving (a bulk delivery, an import) the list, the folder counts and an open
|
|
17
|
+
* thread are read about once in this time - a trailing refresh then picks up whatever came since - instead of every debounce window. */
|
|
18
|
+
export const LIVE_EVENT_MIN_INTERVAL_MS = 1500;
|
|
16
19
|
export const NO_LIVE_UPDATES = { tick: 0, folderUids: null };
|
|
17
20
|
/** The class names `NotificationUtils.sendMessage()` publishes are the model's (`MessageMongo`, `MessageSQL`, `FolderMongo`). */
|
|
18
21
|
const MESSAGE_EVENT = /^Message/;
|
|
@@ -88,9 +91,11 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
|
|
|
88
91
|
let pendingFolders = new Set();
|
|
89
92
|
let pendingUnknown = false;
|
|
90
93
|
let everOpen = false;
|
|
94
|
+
let lastFlushAt = -Infinity;
|
|
91
95
|
// Never runs once `stopped`: whatever sets it clears the timer first.
|
|
92
96
|
function flush() {
|
|
93
97
|
timer = undefined;
|
|
98
|
+
lastFlushAt = Date.now();
|
|
94
99
|
const folderUids = pendingUnknown ? null : new Set(pendingFolders);
|
|
95
100
|
pendingFolders = new Set();
|
|
96
101
|
pendingUnknown = false;
|
|
@@ -108,7 +113,7 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
|
|
|
108
113
|
else {
|
|
109
114
|
pendingUnknown = true;
|
|
110
115
|
}
|
|
111
|
-
timer ?? (timer = setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS));
|
|
116
|
+
timer ?? (timer = setTimeout(flush, Math.max(LIVE_EVENT_DEBOUNCE_MS, lastFlushAt + LIVE_EVENT_MIN_INTERVAL_MS - Date.now())));
|
|
112
117
|
}
|
|
113
118
|
const offEvent = client.onEvent((event) => {
|
|
114
119
|
const sendEvent = parseSendEvent(event);
|
|
@@ -7,6 +7,8 @@ export interface EmptiableFolder {
|
|
|
7
7
|
name: string;
|
|
8
8
|
/** Every folder to empty, when "the folder" is the "All mailboxes" view of one type (each mailbox's own Deleted Items): `uid` is then the first of them. */
|
|
9
9
|
uids?: string[];
|
|
10
|
+
/** The names of the mailboxes those folders are in, for the confirmation to say whose are emptied (in the "All mailboxes" view). */
|
|
11
|
+
scope?: string[];
|
|
10
12
|
}
|
|
11
13
|
export interface PermanentDelete {
|
|
12
14
|
/**
|
|
@@ -97,7 +97,7 @@ export function usePermanentDelete() {
|
|
|
97
97
|
? "all items"
|
|
98
98
|
: folderRequest.count === 1
|
|
99
99
|
? "the only item"
|
|
100
|
-
: `all ${folderRequest.count} items`} in ${folderRequest.folder.name}? This can't be undone.`
|
|
100
|
+
: `all ${folderRequest.count} items`} in ${folderRequest.folder.name}${folderRequest.folder.scope ? ` of: ${folderRequest.folder.scope.join(", ")}` : ""}? This can't be undone.`
|
|
101
101
|
: `Permanently delete ${messageCount(request?.kind === "messages" ? request.messages.length : 0)}? This can't be undone.`, confirmLabel: folderRequest ? "Delete all permanently" : "Delete permanently", busy: running, onConfirm: () => void confirm(), onCancel: cancel }));
|
|
102
102
|
return { requestPermanentDelete, requestEmptyFolder, busy: pending !== null, dialog };
|
|
103
103
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { logSessionEvent } from "../../../lib/auth/session.js";
|
|
5
6
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
6
7
|
import { describeSendFailure } from "../../../lib/mail/sendFailure.js";
|
|
7
8
|
import { notify } from "./store.js";
|
|
@@ -16,6 +17,7 @@ export function setSignInUrl(authServerUrl) {
|
|
|
16
17
|
}
|
|
17
18
|
/** The "Your session expired" pop-up: one however many requests noticed, with a "Sign in" action. Returns its id. */
|
|
18
19
|
export function notifySessionExpired() {
|
|
20
|
+
logSessionEvent("notice:session-expired");
|
|
19
21
|
return notify({
|
|
20
22
|
kind: "error",
|
|
21
23
|
title: "Your session expired",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The user's one switch for every pop-up (new mail, a failed send, an API error, a reminder), per browser in `localStorage`.
|
|
3
3
|
* Framework-free like the store, which reads it: `notify()` keeps a notification out of sight while it is off (it is still
|
|
4
|
-
* listed in "Recent notifications").
|
|
4
|
+
* listed in "Recent notifications") - except an error that carries actions (Retry, Open draft), which would otherwise be unreachable.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* `localStorage` key of the switch. Absent means on; `"off"` means the user turned pop-ups off. The name is from when the switch
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* The user's one switch for every pop-up (new mail, a failed send, an API error, a reminder), per browser in `localStorage`.
|
|
7
7
|
* Framework-free like the store, which reads it: `notify()` keeps a notification out of sight while it is off (it is still
|
|
8
|
-
* listed in "Recent notifications").
|
|
8
|
+
* listed in "Recent notifications") - except an error that carries actions (Retry, Open draft), which would otherwise be unreachable.
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* `localStorage` key of the switch. Absent means on; `"off"` means the user turned pop-ups off. The name is from when the switch
|
|
@@ -232,7 +232,9 @@ export function notify(input) {
|
|
|
232
232
|
paused: false,
|
|
233
233
|
};
|
|
234
234
|
applyInput(entry, input);
|
|
235
|
-
|
|
235
|
+
// An error that offers something to do (a failed send's Retry and Open draft, "Sign in again") is shown whatever the switch says: it is
|
|
236
|
+
// the only way back to what the person typed, and the history has no way to carry out its actions.
|
|
237
|
+
if (!getNotificationsEnabled() && !(entry.kind === "error" && entry.actions.length > 0)) {
|
|
236
238
|
// The user has pop-ups off: nothing is shown, but it is listed in the history like any other.
|
|
237
239
|
record(entry);
|
|
238
240
|
emit();
|
|
@@ -35,7 +35,8 @@ export interface PluginNavProps {
|
|
|
35
35
|
pluginNav?: PluginNav;
|
|
36
36
|
}
|
|
37
37
|
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
38
|
-
* protocol-relative URLs to another host)
|
|
38
|
+
* protocol-relative URLs to another host), and holds no control character, space or backslash anywhere -
|
|
39
|
+
* URL parsers strip tab/CR/LF from anywhere, so `/<tab>/evil.com` would otherwise read as `//evil.com`. */
|
|
39
40
|
export declare function isSafePluginHref(href: string): boolean;
|
|
40
41
|
/**
|
|
41
42
|
* Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
6
|
-
* protocol-relative URLs to another host)
|
|
6
|
+
* protocol-relative URLs to another host), and holds no control character, space or backslash anywhere -
|
|
7
|
+
* URL parsers strip tab/CR/LF from anywhere, so `/<tab>/evil.com` would otherwise read as `//evil.com`. */
|
|
7
8
|
export function isSafePluginHref(href) {
|
|
8
|
-
|
|
9
|
+
// eslint-disable-next-line no-control-regex -- the control characters are exactly what is being rejected
|
|
10
|
+
return /^\/(?![/\\])[^\u0000-\u0020\u007f\\]*$/.test(href);
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
9
9
|
import { HiOutlineBars3, HiOutlinePlus } from "react-icons/hi2";
|
|
10
10
|
import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, parseISO, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
|
|
@@ -17,6 +17,7 @@ import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calen
|
|
|
17
17
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
18
18
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
19
19
|
import { expandAllOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
20
|
+
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
20
21
|
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
21
22
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
22
23
|
import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
@@ -46,7 +47,7 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
46
47
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
47
48
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
48
49
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
49
|
-
import {
|
|
50
|
+
import { changedEventOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
50
51
|
/** The furthest instants a `Date` can hold: the List's "range" reaches both. */
|
|
51
52
|
const MAX_DATE_MS = 8.64e15;
|
|
52
53
|
function CalendarPage(props) {
|
|
@@ -236,15 +237,20 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
236
237
|
const calendarFolderUidsKey = calendarFolders.map((f) => f.uid).sort().join(",");
|
|
237
238
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
238
239
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
239
|
-
//
|
|
240
|
-
// see (see `
|
|
241
|
-
// reader may see - all of it for the owner - and replaces (or joins) what the calendar holds; one that is gone (a 404) leaves it. A failed
|
|
242
|
-
// changes nothing: the next load shows the event as it is.
|
|
240
|
+
// An event that somebody creates, changes or deletes is announced on the push connection - a private or confidential one as its busy block, whatever the
|
|
241
|
+
// reader may see (see `changedEventOf()`): that payload is never stored here. The event is fetched again by its uid, which comes back as much as this
|
|
242
|
+
// reader may see - all of it for the owner - and replaces (or joins) what the calendar holds; one that is gone (a 404, or a delete) leaves it. A failed
|
|
243
|
+
// fetch changes nothing: the next load shows the event as it is.
|
|
243
244
|
const checkedFolderUidsRef = useRef(checkedFolderUids);
|
|
244
245
|
checkedFolderUidsRef.current = checkedFolderUids;
|
|
245
246
|
useEffect(() => getPushClient().onEvent((event) => {
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
247
|
+
const change = changedEventOf(event);
|
|
248
|
+
if (!change) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const { uid } = change;
|
|
252
|
+
if (change.deleted) {
|
|
253
|
+
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
248
254
|
return;
|
|
249
255
|
}
|
|
250
256
|
void getCalendarEvent(uid, client).then((fresh) => {
|
|
@@ -272,11 +278,14 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
272
278
|
return () => clearTimeout(timer);
|
|
273
279
|
}, [searchText]);
|
|
274
280
|
// The List shows only what is from today on, so that is all a search steps through there; the other views reach the past ones.
|
|
281
|
+
// Today moves on at midnight: the List's "from today on" is worked out again then (`listDay` is only a dependency).
|
|
282
|
+
const listDay = useDayKey();
|
|
283
|
+
const dayOfList = view === "list" ? listDay : "";
|
|
275
284
|
const matches = useMemo(() => {
|
|
276
285
|
const found = searching ? searchOccurrences(events, query) : [];
|
|
277
286
|
const today = new Date();
|
|
278
287
|
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
279
|
-
}, [events, query, searching, view]);
|
|
288
|
+
}, [events, query, searching, view, dayOfList]);
|
|
280
289
|
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
281
290
|
// What an open event card steps through, in order: the matches while a search runs, else every event of the checked calendars (bounded as a
|
|
282
291
|
// search is). The List holds only what is from today on, so that is all its card steps through. Worked out only when something needs it.
|
|
@@ -290,7 +299,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
290
299
|
return everything.filter((occurrence) => isUpcoming(occurrence, today));
|
|
291
300
|
}
|
|
292
301
|
return everything;
|
|
293
|
-
}, [searching, matches, view, everything]);
|
|
302
|
+
}, [searching, matches, view, everything, dayOfList]);
|
|
294
303
|
const occurrences = useMemo(() => {
|
|
295
304
|
if (view === "list") {
|
|
296
305
|
return sequence;
|
|
@@ -425,11 +434,12 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
425
434
|
initialAllDay,
|
|
426
435
|
});
|
|
427
436
|
}
|
|
428
|
-
|
|
437
|
+
// Stable (it only sets state), so the List's rows are not rendered again by every keystroke in the search box.
|
|
438
|
+
const openEvent = useCallback((occurrence) => {
|
|
429
439
|
setSlide(undefined);
|
|
430
440
|
setActiveKey(occurrence.occurrenceKey);
|
|
431
441
|
setModal({ occurrence });
|
|
432
|
-
}
|
|
442
|
+
}, []);
|
|
433
443
|
function closeModal() {
|
|
434
444
|
setModal(null);
|
|
435
445
|
}
|
|
@@ -477,6 +487,8 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
477
487
|
}
|
|
478
488
|
catch (err) {
|
|
479
489
|
notifyApiError(err, action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event");
|
|
490
|
+
// A refused change (a 409 most of all) means the page holds a stale version: load again, or every later drag of this event fails the same way.
|
|
491
|
+
reload();
|
|
480
492
|
}
|
|
481
493
|
}
|
|
482
494
|
const title = view === "list"
|