@rapidmx/web-client 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +21 -2
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useSyncExternalStore } from "react";
|
|
6
|
+
import { gravatarEnabled, setGravatarEnabled, subscribeGravatarPreference } from "../../../../lib/contacts/gravatar.js";
|
|
7
|
+
|
|
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 (
|
|
15
|
+
<div>
|
|
16
|
+
<h2 className="text-sm font-semibold mb-2">Contact pictures</h2>
|
|
17
|
+
<label className="flex items-center gap-2 text-sm">
|
|
18
|
+
<input type="checkbox" checked={enabled} onChange={(e) => setGravatarEnabled(e.target.checked)} />
|
|
19
|
+
Show profile pictures from Gravatar
|
|
20
|
+
</label>
|
|
21
|
+
<p className="text-xs text-text-muted mt-2">
|
|
22
|
+
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
|
|
23
|
+
default; applies to this browser only.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -156,7 +156,7 @@ export default function SettingsShell({
|
|
|
156
156
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
157
157
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
158
158
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
159
|
-
return <MailboxProvisioning />;
|
|
159
|
+
return <MailboxProvisioning impersonating={impersonating} impersonationBaseUrl={impersonationBaseUrl} />;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
let inner: ReactNode = null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { parseISO } from "date-fns";
|
|
6
|
+
|
|
7
|
+
const MS_PER_DAY = 86_400_000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 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
|
|
11
|
+
* 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
|
|
12
|
+
* different day for a reader in another zone.)
|
|
13
|
+
*/
|
|
14
|
+
export function dueDateInstant(dateKey: string): string {
|
|
15
|
+
return `${dateKey}T00:00:00.000Z`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 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
|
|
20
|
+
* 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
|
|
21
|
+
* midnight - is that instant itself. (`parseISO()` rather than `new Date()`: a bare `yyyy-MM-dd` would be read as UTC.)
|
|
22
|
+
*/
|
|
23
|
+
export function parseDueDate(value: string): Date {
|
|
24
|
+
const instant = parseISO(value);
|
|
25
|
+
if (instant.getTime() % MS_PER_DAY !== 0) {
|
|
26
|
+
return instant;
|
|
27
|
+
}
|
|
28
|
+
return new Date(instant.getUTCFullYear(), instant.getUTCMonth(), instant.getUTCDate());
|
|
29
|
+
}
|
|
@@ -116,7 +116,7 @@ export default function TasksShell({
|
|
|
116
116
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
117
117
|
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
118
118
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
119
|
-
return <MailboxProvisioning />;
|
|
119
|
+
return <MailboxProvisioning impersonating={impersonating} impersonationBaseUrl={impersonationBaseUrl} />;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
let inner: ReactNode = null;
|
|
@@ -5,6 +5,7 @@
|
|
|
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
|
|
|
9
10
|
/**
|
|
10
11
|
* What this device has recovered of encrypted messages by decrypting them: the real subject (the server only ever holds RFC 9788's `[...]`
|
|
@@ -26,6 +27,50 @@ let unlockEpoch = 0;
|
|
|
26
27
|
const listeners = new Set<() => void>();
|
|
27
28
|
const inFlight = new Set<string>();
|
|
28
29
|
|
|
30
|
+
/** 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). */
|
|
31
|
+
const DECRYPT_RETRY_MS = 10 * 60_000;
|
|
32
|
+
/** How many such messages are remembered. */
|
|
33
|
+
const MAX_FAILED = 1000;
|
|
34
|
+
/** The messages whose decrypting came to nothing (fetch failed, not for this key, nothing readable), by uid: when and in which unlock epoch. */
|
|
35
|
+
const failed = new Map<string, { epoch: number; at: number }>();
|
|
36
|
+
|
|
37
|
+
/** Whether decrypting this message already came to nothing since the keys were last unlocked, recently enough that trying again would only repeat it. */
|
|
38
|
+
export function decryptFailedBefore(uid: string): boolean {
|
|
39
|
+
const known = failed.get(uid);
|
|
40
|
+
return !!known && known.epoch === unlockEpoch && Date.now() - known.at < DECRYPT_RETRY_MS;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Remembers that decrypting this message came to nothing, so a list that refreshes does not download and try it again each time. */
|
|
44
|
+
export function markDecryptFailed(uid: string): void {
|
|
45
|
+
failed.delete(uid);
|
|
46
|
+
if (failed.size >= MAX_FAILED) {
|
|
47
|
+
failed.delete(failed.keys().next().value!);
|
|
48
|
+
}
|
|
49
|
+
failed.set(uid, { epoch: unlockEpoch, at: Date.now() });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Forgets everything remembered about messages that could not be decrypted (sign-out, an account switch). */
|
|
53
|
+
export function clearDecryptedMessages(): void {
|
|
54
|
+
entries = {};
|
|
55
|
+
failed.clear();
|
|
56
|
+
emit();
|
|
57
|
+
}
|
|
58
|
+
onSigningOut(clearDecryptedMessages);
|
|
59
|
+
|
|
60
|
+
/** Runs `task` over `items` with at most `limit` running at once, resolving to the results in the order of `items`. */
|
|
61
|
+
export async function mapWithConcurrency<T, R>(items: T[], limit: number, task: (item: T) => Promise<R>): Promise<R[]> {
|
|
62
|
+
const results = new Array<R>(items.length);
|
|
63
|
+
let next = 0;
|
|
64
|
+
async function worker(): Promise<void> {
|
|
65
|
+
while (next < items.length) {
|
|
66
|
+
const index = next++;
|
|
67
|
+
results[index] = await task(items[index]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
29
74
|
function emit(): void {
|
|
30
75
|
for (const listener of [...listeners]) {
|
|
31
76
|
listener();
|
|
@@ -96,7 +141,7 @@ export function useUnlockEpoch(): number {
|
|
|
96
141
|
*/
|
|
97
142
|
export async function decryptForDisplay(uid: string, mailboxUid: string): Promise<void> {
|
|
98
143
|
const unlocked = getUnlockedKeys(mailboxUid);
|
|
99
|
-
if (!unlocked || entries[uid] || inFlight.has(uid)) {
|
|
144
|
+
if (!unlocked || entries[uid] || inFlight.has(uid) || decryptFailedBefore(uid)) {
|
|
100
145
|
return;
|
|
101
146
|
}
|
|
102
147
|
inFlight.add(uid);
|
|
@@ -107,9 +152,13 @@ export async function decryptForDisplay(uid: string, mailboxUid: string): Promis
|
|
|
107
152
|
// A lock while this ran: the result must not go back on screen.
|
|
108
153
|
if (getUnlockedKeys(mailboxUid) === unlocked) {
|
|
109
154
|
rememberDecrypted(uid, mailboxUid, { subject: security.subject, preview: security.html ? previewOf(security.html) : security.text?.slice(0, PREVIEW_MAX_LENGTH) });
|
|
155
|
+
if (!security.subject && !security.html && !security.text) {
|
|
156
|
+
markDecryptFailed(uid);
|
|
157
|
+
}
|
|
110
158
|
}
|
|
111
159
|
} catch {
|
|
112
|
-
// Left as the placeholder.
|
|
160
|
+
// Left as the placeholder - and not fetched again until the next unlock.
|
|
161
|
+
markDecryptFailed(uid);
|
|
113
162
|
} finally {
|
|
114
163
|
inFlight.delete(uid);
|
|
115
164
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import type { ConversationSummary } from "../../../lib/mail/conversationsApi.js";
|
|
6
6
|
import type { Message } from "../../../lib/mail/mailApi.js";
|
|
7
|
+
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A short-lived, in-memory copy of a folder's listing, so that switching to a folder that was shown a moment ago (or coming back
|
|
@@ -85,3 +86,4 @@ export function saveListScroll(key: string, scrollTop: number): void {
|
|
|
85
86
|
export function clearListSnapshots(): void {
|
|
86
87
|
snapshots.clear();
|
|
87
88
|
}
|
|
89
|
+
onSigningOut(clearListSnapshots);
|
|
@@ -82,9 +82,12 @@ export interface SendDecisionInput {
|
|
|
82
82
|
* can choose, or rejected outright. Used twice with the same rules - by the compose window, from what it already knows, to catch a problem
|
|
83
83
|
* *before* it closes (`lookupsComplete: false`), and by the background send, once its recipient lookups are in.
|
|
84
84
|
*
|
|
85
|
-
* Encryption follows `evaluateEncryptionRequirement()`
|
|
86
|
-
* encrypts, is encrypted; everything unknown is plain
|
|
87
|
-
*
|
|
85
|
+
* Encryption follows `evaluateEncryptionRequirement()`. The compose window's early check (`lookupsComplete: false`) is fail open: only a message
|
|
86
|
+
* the user asked to encrypt, or one the loaded policy encrypts, is encrypted there; everything unknown is plain, so a flapping gateway never
|
|
87
|
+
* 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
|
|
88
|
+
* 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
|
|
89
|
+
* 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
|
|
90
|
+
* choose to send it without encryption.
|
|
88
91
|
*/
|
|
89
92
|
export function decideSend(input: SendDecisionInput): SendDecision {
|
|
90
93
|
const { keys } = input;
|
|
@@ -122,6 +125,14 @@ export function decideSend(input: SendDecisionInput): SendDecision {
|
|
|
122
125
|
});
|
|
123
126
|
const encrypting = requirement.required;
|
|
124
127
|
|
|
128
|
+
if (!encrypting && input.lookupsComplete && requirement.unknown.some((fact) => fact !== "mailbox")) {
|
|
129
|
+
// 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
|
|
130
|
+
// 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
|
|
131
|
+
// *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
|
|
132
|
+
// stop everyone's mail.)
|
|
133
|
+
return { action: "blocked", block: { kind: "lookup-failed", message: LOOKUP_UNAVAILABLE_MESSAGE, overrideLabel: "Send without encryption", keysLocked: false } };
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
if (encrypting) {
|
|
126
137
|
const statuses = input.recipients.filter((status): status is RecipientEncryptionStatus => !!status);
|
|
127
138
|
const allResolved = statuses.length === input.recipients.length;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Message,
|
|
12
12
|
assembleDraft,
|
|
13
13
|
assembleDraftRaw,
|
|
14
|
+
forgetDraftImages,
|
|
14
15
|
getMailbox,
|
|
15
16
|
getMessage,
|
|
16
17
|
queueMessageSend,
|
|
@@ -89,8 +90,10 @@ export function retainedRequest(draftUid: string): SendRequest | undefined {
|
|
|
89
90
|
return sendState.retained.get(draftUid);
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/** 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. */
|
|
92
94
|
export function forgetRetainedRequest(draftUid: string): void {
|
|
93
95
|
sendState.retained.delete(draftUid);
|
|
96
|
+
forgetDraftImages(draftUid);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
function retain(request: SendRequest): void {
|
|
@@ -424,8 +427,10 @@ export function startSend(request: SendRequest, client?: ApiClient): boolean {
|
|
|
424
427
|
} else if (outcome.kind === "sent") {
|
|
425
428
|
// A server with no queue relayed it before answering: the Sent Items copy exists already.
|
|
426
429
|
markOutgoingSent(uid);
|
|
430
|
+
forgetDraftImages(uid);
|
|
427
431
|
notifySent();
|
|
428
432
|
} else {
|
|
433
|
+
forgetDraftImages(uid);
|
|
429
434
|
notify({
|
|
430
435
|
id: `scheduled:${uid}`,
|
|
431
436
|
kind: "success",
|
|
@@ -18,6 +18,10 @@ export const LIVE_POLL_INTERVAL_MS = 45_000;
|
|
|
18
18
|
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
19
19
|
export const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
20
20
|
|
|
21
|
+
/** 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
|
|
22
|
+
* thread are read about once in this time - a trailing refresh then picks up whatever came since - instead of every debounce window. */
|
|
23
|
+
export const LIVE_EVENT_MIN_INTERVAL_MS = 1_500;
|
|
24
|
+
|
|
21
25
|
/** What changed, as far as a list on screen is concerned. */
|
|
22
26
|
export interface LiveUpdates {
|
|
23
27
|
/** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
|
|
@@ -153,10 +157,12 @@ export function useMailLiveUpdates({
|
|
|
153
157
|
let pendingFolders = new Set<string>();
|
|
154
158
|
let pendingUnknown = false;
|
|
155
159
|
let everOpen = false;
|
|
160
|
+
let lastFlushAt = -Infinity;
|
|
156
161
|
|
|
157
162
|
// Never runs once `stopped`: whatever sets it clears the timer first.
|
|
158
163
|
function flush() {
|
|
159
164
|
timer = undefined;
|
|
165
|
+
lastFlushAt = Date.now();
|
|
160
166
|
const folderUids = pendingUnknown ? null : new Set(pendingFolders);
|
|
161
167
|
pendingFolders = new Set();
|
|
162
168
|
pendingUnknown = false;
|
|
@@ -174,7 +180,7 @@ export function useMailLiveUpdates({
|
|
|
174
180
|
} else {
|
|
175
181
|
pendingUnknown = true;
|
|
176
182
|
}
|
|
177
|
-
timer ??= setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS);
|
|
183
|
+
timer ??= setTimeout(flush, Math.max(LIVE_EVENT_DEBOUNCE_MS, lastFlushAt + LIVE_EVENT_MIN_INTERVAL_MS - Date.now()));
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
const offEvent = client.onEvent((event) => {
|
|
@@ -25,6 +25,8 @@ export interface EmptiableFolder {
|
|
|
25
25
|
name: string;
|
|
26
26
|
/** 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. */
|
|
27
27
|
uids?: string[];
|
|
28
|
+
/** The names of the mailboxes those folders are in, for the confirmation to say whose are emptied (in the "All mailboxes" view). */
|
|
29
|
+
scope?: string[];
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface PermanentDelete {
|
|
@@ -155,7 +157,7 @@ export function usePermanentDelete(): PermanentDelete {
|
|
|
155
157
|
: folderRequest.count === 1
|
|
156
158
|
? "the only item"
|
|
157
159
|
: `all ${folderRequest.count} items`
|
|
158
|
-
} in ${folderRequest.folder.name}? This can't be undone.`
|
|
160
|
+
} in ${folderRequest.folder.name}${folderRequest.folder.scope ? ` of: ${folderRequest.folder.scope.join(", ")}` : ""}? This can't be undone.`
|
|
159
161
|
: `Permanently delete ${messageCount(request?.kind === "messages" ? request.messages.length : 0)}? This can't be undone.`
|
|
160
162
|
}
|
|
161
163
|
confirmLabel={folderRequest ? "Delete all permanently" : "Delete permanently"}
|
|
@@ -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 { NotificationAction, notify } from "./store.js";
|
|
@@ -20,6 +21,7 @@ export function setSignInUrl(authServerUrl: string | undefined): void {
|
|
|
20
21
|
|
|
21
22
|
/** The "Your session expired" pop-up: one however many requests noticed, with a "Sign in" action. Returns its id. */
|
|
22
23
|
export function notifySessionExpired(): string {
|
|
24
|
+
logSessionEvent("notice:session-expired");
|
|
23
25
|
return notify({
|
|
24
26
|
kind: "error",
|
|
25
27
|
title: "Your session expired",
|
|
@@ -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
|
/**
|
|
@@ -382,7 +382,9 @@ export function notify(input: NotifyInput): string {
|
|
|
382
382
|
paused: false,
|
|
383
383
|
};
|
|
384
384
|
applyInput(entry, input);
|
|
385
|
-
|
|
385
|
+
// 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
|
|
386
|
+
// the only way back to what the person typed, and the history has no way to carry out its actions.
|
|
387
|
+
if (!getNotificationsEnabled() && !(entry.kind === "error" && entry.actions.length > 0)) {
|
|
386
388
|
// The user has pop-ups off: nothing is shown, but it is listed in the history like any other.
|
|
387
389
|
record(entry);
|
|
388
390
|
emit();
|
|
@@ -43,9 +43,11 @@ export interface PluginNavProps {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
46
|
-
* protocol-relative URLs to another host)
|
|
46
|
+
* protocol-relative URLs to another host), and holds no control character, space or backslash anywhere -
|
|
47
|
+
* URL parsers strip tab/CR/LF from anywhere, so `/<tab>/evil.com` would otherwise read as `//evil.com`. */
|
|
47
48
|
export function isSafePluginHref(href: string): boolean {
|
|
48
|
-
|
|
49
|
+
// eslint-disable-next-line no-control-regex -- the control characters are exactly what is being rejected
|
|
50
|
+
return /^\/(?![/\\])[^\u0000-\u0020\u007f\\]*$/.test(href);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
6
|
-
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { DndContext, DragEndEvent, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
8
8
|
import { HiOutlineBars3, HiOutlinePlus } from "react-icons/hi2";
|
|
9
9
|
import {
|
|
@@ -29,6 +29,7 @@ import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calen
|
|
|
29
29
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
30
30
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
31
31
|
import { CalendarOccurrence, expandAllOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
32
|
+
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
32
33
|
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
33
34
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
34
35
|
import CalendarShell, { CalendarShellProps, useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
@@ -59,7 +60,7 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
59
60
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
60
61
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
61
62
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
62
|
-
import {
|
|
63
|
+
import { changedEventOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
63
64
|
|
|
64
65
|
/** The furthest instants a `Date` can hold: the List's "range" reaches both. */
|
|
65
66
|
const MAX_DATE_MS = 8.64e15;
|
|
@@ -294,17 +295,22 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
294
295
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
295
296
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
296
297
|
|
|
297
|
-
//
|
|
298
|
-
// see (see `
|
|
299
|
-
// 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
|
|
300
|
-
// changes nothing: the next load shows the event as it is.
|
|
298
|
+
// 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
|
|
299
|
+
// 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
|
|
300
|
+
// 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
|
|
301
|
+
// fetch changes nothing: the next load shows the event as it is.
|
|
301
302
|
const checkedFolderUidsRef = useRef(checkedFolderUids);
|
|
302
303
|
checkedFolderUidsRef.current = checkedFolderUids;
|
|
303
304
|
useEffect(
|
|
304
305
|
() =>
|
|
305
306
|
getPushClient().onEvent((event) => {
|
|
306
|
-
const
|
|
307
|
-
if (!
|
|
307
|
+
const change = changedEventOf(event);
|
|
308
|
+
if (!change) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const { uid } = change;
|
|
312
|
+
if (change.deleted) {
|
|
313
|
+
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
308
314
|
return;
|
|
309
315
|
}
|
|
310
316
|
void getCalendarEvent(uid, client).then(
|
|
@@ -340,11 +346,14 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
340
346
|
return () => clearTimeout(timer);
|
|
341
347
|
}, [searchText]);
|
|
342
348
|
// 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.
|
|
349
|
+
// Today moves on at midnight: the List's "from today on" is worked out again then (`listDay` is only a dependency).
|
|
350
|
+
const listDay = useDayKey();
|
|
351
|
+
const dayOfList = view === "list" ? listDay : "";
|
|
343
352
|
const matches = useMemo(() => {
|
|
344
353
|
const found = searching ? searchOccurrences(events, query) : [];
|
|
345
354
|
const today = new Date();
|
|
346
355
|
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
347
|
-
}, [events, query, searching, view]);
|
|
356
|
+
}, [events, query, searching, view, dayOfList]);
|
|
348
357
|
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
349
358
|
|
|
350
359
|
// 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
|
|
@@ -359,7 +368,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
359
368
|
return everything.filter((occurrence) => isUpcoming(occurrence, today));
|
|
360
369
|
}
|
|
361
370
|
return everything;
|
|
362
|
-
}, [searching, matches, view, everything]);
|
|
371
|
+
}, [searching, matches, view, everything, dayOfList]);
|
|
363
372
|
|
|
364
373
|
const occurrences = useMemo(() => {
|
|
365
374
|
if (view === "list") {
|
|
@@ -518,11 +527,12 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
518
527
|
});
|
|
519
528
|
}
|
|
520
529
|
|
|
521
|
-
|
|
530
|
+
// Stable (it only sets state), so the List's rows are not rendered again by every keystroke in the search box.
|
|
531
|
+
const openEvent = useCallback((occurrence: CalendarOccurrence) => {
|
|
522
532
|
setSlide(undefined);
|
|
523
533
|
setActiveKey(occurrence.occurrenceKey);
|
|
524
534
|
setModal({ occurrence });
|
|
525
|
-
}
|
|
535
|
+
}, []);
|
|
526
536
|
|
|
527
537
|
function closeModal() {
|
|
528
538
|
setModal(null);
|
|
@@ -573,6 +583,8 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
573
583
|
reload();
|
|
574
584
|
} catch (err) {
|
|
575
585
|
notifyApiError(err, action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event");
|
|
586
|
+
// 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.
|
|
587
|
+
reload();
|
|
576
588
|
}
|
|
577
589
|
}
|
|
578
590
|
|
|
@@ -8,12 +8,13 @@ import React, { useEffect, useState } from "react";
|
|
|
8
8
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
9
9
|
import { Contact, deleteContact, getContact } from "../../../lib/contacts/contactsApi.js";
|
|
10
10
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
11
|
-
import ContactsShell, { ContactsShellProps } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
11
|
+
import ContactsShell, { ContactsShellProps, useContactsShell } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
12
12
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
13
13
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
14
14
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
15
15
|
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
16
16
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
17
|
+
import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
@@ -38,6 +39,9 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
38
39
|
const [loading, setLoading] = useState(true);
|
|
39
40
|
const [error, setError] = useState<string | null>(null);
|
|
40
41
|
const [mode, setMode] = useState<Mode>("view");
|
|
42
|
+
const { mailboxes } = useContactsShell();
|
|
43
|
+
// A contact in a mailbox shared with the reader view-only can be read, not changed.
|
|
44
|
+
const writable = useMailboxUpdateAccess(mailboxes.find((mailbox) => mailbox.uid === contact?.mailboxUid));
|
|
41
45
|
|
|
42
46
|
useEffect(() => {
|
|
43
47
|
setLoading(true);
|
|
@@ -101,6 +105,7 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
101
105
|
contact={contact}
|
|
102
106
|
onEdit={() => setMode("edit")}
|
|
103
107
|
onDelete={() => handleDelete(contact)}
|
|
108
|
+
canWrite={writable}
|
|
104
109
|
backHref={backHref}
|
|
105
110
|
onKeysChanged={handleKeysChanged}
|
|
106
111
|
onChanged={setContact}
|