@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
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
createContact,
|
|
12
12
|
deleteContact,
|
|
13
13
|
listContacts,
|
|
14
|
+
listContactsInFolders,
|
|
14
15
|
listDeletedContacts,
|
|
15
16
|
setContactFavorite,
|
|
16
|
-
contactPhotoUrl,
|
|
17
17
|
updateContact,
|
|
18
18
|
} from "../../../lib/contacts/contactsApi.js";
|
|
19
|
-
import { contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
19
|
+
import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, ParsedVCardContact, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
20
|
+
import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
|
|
20
21
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
21
22
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
22
23
|
import { useCompose } from "../../shared/components/mail/compose/ComposeContext.js";
|
|
@@ -25,11 +26,12 @@ import ContactsSidebar, { ContactsView } from "../../shared/components/contacts/
|
|
|
25
26
|
import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js";
|
|
26
27
|
import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
|
|
27
28
|
import { HiOutlineUserPlus } from "react-icons/hi2";
|
|
28
|
-
import
|
|
29
|
+
import ContactPhotoAvatar from "../../../lib/components/avatar/ContactPhotoAvatar.js";
|
|
29
30
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
30
31
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
31
32
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
32
33
|
import { getMyMailboxAccess } from "../../../lib/mail/mailboxAccessApi.js";
|
|
34
|
+
import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
|
|
33
35
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
34
36
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
35
37
|
import Modal from "../../../lib/components/overlays/Modal.js";
|
|
@@ -38,6 +40,7 @@ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigne
|
|
|
38
40
|
import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
39
41
|
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
40
42
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
43
|
+
import { notify } from "../../shared/notifications/store.js";
|
|
41
44
|
|
|
42
45
|
const INPUT_CLASS =
|
|
43
46
|
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -80,6 +83,8 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
80
83
|
const { folderUid, mailboxUid, mailboxes } = useContactsShell();
|
|
81
84
|
// The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
|
|
82
85
|
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
|
|
86
|
+
// A mailbox shared with the reader view-only: its contacts can be read, not changed.
|
|
87
|
+
const mailboxWritable = useMailboxUpdateAccess(mailboxes.find((mb) => mb.uid === mailboxUid));
|
|
83
88
|
const { openCompose } = useCompose();
|
|
84
89
|
const isMobile = useIsMobile();
|
|
85
90
|
const navigate = useNavigate();
|
|
@@ -309,6 +314,22 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
// The bulk handlers below try every contact and report each failure: the same failure repeated is one pop-up with a count.
|
|
317
|
+
|
|
318
|
+
// A bulk action sends every checked row's own version: pressed again before it finishes, it would send stale ones and raise a stream of 409s.
|
|
319
|
+
const bulkRunning = useRef(false);
|
|
320
|
+
function singleFlight(run: () => Promise<void>): () => Promise<void> {
|
|
321
|
+
return async () => {
|
|
322
|
+
if (bulkRunning.current) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
bulkRunning.current = true;
|
|
326
|
+
try {
|
|
327
|
+
await run();
|
|
328
|
+
} finally {
|
|
329
|
+
bulkRunning.current = false;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
312
333
|
async function handleBulkDelete() {
|
|
313
334
|
setConfirmingBulkDelete(false);
|
|
314
335
|
for (const contact of checkedContacts) {
|
|
@@ -339,7 +360,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
339
360
|
openCompose({ to: addresses.join(", ") });
|
|
340
361
|
}
|
|
341
362
|
|
|
342
|
-
|
|
363
|
+
const handleToggleFavorite = singleFlight(async () => {
|
|
343
364
|
const allFavorited = checkedContacts.every((c) => c.favorite);
|
|
344
365
|
for (const contact of checkedContacts) {
|
|
345
366
|
try {
|
|
@@ -350,9 +371,9 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
350
371
|
}
|
|
351
372
|
clearPinnedSignerCache();
|
|
352
373
|
await reload();
|
|
353
|
-
}
|
|
374
|
+
});
|
|
354
375
|
|
|
355
|
-
|
|
376
|
+
const handleAddCategory = singleFlight(async () => {
|
|
356
377
|
const category = window.prompt("Category name");
|
|
357
378
|
if (!category?.trim()) {
|
|
358
379
|
return;
|
|
@@ -369,7 +390,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
369
390
|
}
|
|
370
391
|
clearPinnedSignerCache();
|
|
371
392
|
await reload();
|
|
372
|
-
}
|
|
393
|
+
});
|
|
373
394
|
|
|
374
395
|
function handleExportVCard() {
|
|
375
396
|
downloadTextFile(
|
|
@@ -382,9 +403,35 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
382
403
|
if (!mailboxUid || !folderUid) {
|
|
383
404
|
return;
|
|
384
405
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
406
|
+
// A file is only read, and only so many records taken from it (see `parseVCards()`): one contact is one request, and a huge export would
|
|
407
|
+
// lock the page for minutes and flood the server. Whatever goes wrong while reading it is said, not left as an unhandled rejection.
|
|
408
|
+
if (file.size > VCARD_MAX_CHARS) {
|
|
409
|
+
notify({ kind: "error", title: "Couldn't import contacts", message: `That file is larger than ${Math.round(VCARD_MAX_CHARS / 1_000_000)} MB. Split it into smaller files.` });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
let parsed: ReturnType<typeof parseVCards>;
|
|
413
|
+
try {
|
|
414
|
+
parsed = parseVCards(await file.text());
|
|
415
|
+
} catch (err) {
|
|
416
|
+
notifyApiError(err, "Couldn't read that contacts file");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (parsed.length >= VCARD_MAX_CONTACTS) {
|
|
420
|
+
notify({ kind: "warning", title: "Only part of the file was imported", message: `A file is imported up to its first ${VCARD_MAX_CONTACTS} contacts - import the rest from another file.` });
|
|
421
|
+
}
|
|
422
|
+
// Importing a file twice must not double every contact: one whose address (or, with no address, name and phone) is already a contact's - or an
|
|
423
|
+
// earlier card's in the file - is left alone. The folder is read in full for that, not just the page of it the list holds.
|
|
424
|
+
let fresh: ParsedVCardContact[];
|
|
425
|
+
try {
|
|
426
|
+
fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
notifyApiError(err, "Couldn't check the file against your contacts");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (fresh.length < parsed.length) {
|
|
432
|
+
notify({ kind: "info", title: "Some contacts were skipped", message: `${parsed.length - fresh.length} of the contacts in that file are already in your contacts.` });
|
|
433
|
+
}
|
|
434
|
+
for (const input of fresh) {
|
|
388
435
|
try {
|
|
389
436
|
await createContact({ mailboxUid, folderUid, ...input }, client);
|
|
390
437
|
} catch (err) {
|
|
@@ -513,8 +560,14 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
513
560
|
</td>
|
|
514
561
|
)}
|
|
515
562
|
<td className="px-3 py-2">
|
|
516
|
-
<button
|
|
517
|
-
|
|
563
|
+
<button
|
|
564
|
+
type="button"
|
|
565
|
+
// A deleted contact has no pane to show (it can't be restored or opened), and its route would answer 404.
|
|
566
|
+
disabled={isDeletedView}
|
|
567
|
+
onClick={() => handleSelectRow(contact)}
|
|
568
|
+
className="flex w-full min-w-0 items-center gap-2 text-left disabled:cursor-default"
|
|
569
|
+
>
|
|
570
|
+
<ContactPhotoAvatar lazy displayName={contact.displayName} size={28} contact={contact} email={contact.emails[0]?.address} />
|
|
518
571
|
<span className="min-w-0 truncate font-medium">
|
|
519
572
|
{contact.displayName}
|
|
520
573
|
{contact.favorite && <span aria-label="Favorite"> ★</span>}
|
|
@@ -563,6 +616,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
563
616
|
contact={selected}
|
|
564
617
|
onEdit={() => setMode("edit")}
|
|
565
618
|
onDelete={() => handleDelete(selected)}
|
|
619
|
+
canWrite={mailboxWritable}
|
|
566
620
|
onKeysChanged={() => void reload()}
|
|
567
621
|
onChanged={() => void reload()}
|
|
568
622
|
canResolveKeys={ownsMailbox || delegateCanUpdate}
|
package/apps/www/index.tsx
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import type { Coverage } from "../shared/search/localIndexWorker.js";
|
|
54
54
|
import { getUnlockedKeys, subscribeKeySession, UnlockedKeys } from "../../lib/crypto/keySession.js";
|
|
55
55
|
import { useMessageAttachments } from "../../lib/mail/mailDetailHooks.js";
|
|
56
|
+
import { settleWithLimit } from "../../lib/util/settleWithLimit.js";
|
|
56
57
|
import useIsMobile from "../../lib/util/useIsMobile.js";
|
|
57
58
|
import MailShell, {
|
|
58
59
|
AggregateFolderType,
|
|
@@ -71,7 +72,7 @@ import { setReadStateMany } from "../shared/mail/messageReadState.js";
|
|
|
71
72
|
import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
|
|
72
73
|
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
|
|
73
74
|
import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
|
|
74
|
-
import { rememberDecrypted } from "../shared/mail/decryptedMessages.js";
|
|
75
|
+
import { decryptFailedBefore, mapWithConcurrency, markDecryptFailed, rememberDecrypted } from "../shared/mail/decryptedMessages.js";
|
|
75
76
|
import ConversationList, { ListedConversation, conversationRowKey } from "../shared/components/mail/ConversationList.js";
|
|
76
77
|
import type { ConversationThreadHead } from "../shared/components/mail/ConversationThreadPane.js";
|
|
77
78
|
import SwipeRow from "../shared/components/mail/SwipeRow.js";
|
|
@@ -104,6 +105,8 @@ import { notifyApiError } from "../shared/notifications/apiErrors.js";
|
|
|
104
105
|
const MESSAGE_PAGE_SIZE = 50;
|
|
105
106
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
106
107
|
const LIST_PREVIEW_MAX_LENGTH = 160;
|
|
108
|
+
/** How many encrypted rows are fetched and decrypted at once. */
|
|
109
|
+
const DECRYPT_CONCURRENCY = 4;
|
|
107
110
|
|
|
108
111
|
/** The literal outer-envelope `Subject` every encrypted message carries server-side - RFC 9788's
|
|
109
112
|
* `hcp_baseline` policy obscures it to this exact string (see `smimeMessage.ts`'s
|
|
@@ -147,11 +150,15 @@ function stripHtmlToText(html: string): string {
|
|
|
147
150
|
* and `handleUnlockList()`), and a single row's fetch/decrypt failure never blocks the rest.
|
|
148
151
|
*/
|
|
149
152
|
async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys): Promise<Record<string, DecryptedRow>> {
|
|
150
|
-
|
|
153
|
+
// A row that already came to nothing since the last unlock is not downloaded again by every refresh of the list.
|
|
154
|
+
const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER && !decryptFailedBefore(m.uid));
|
|
151
155
|
// Imported once for all the rows, by whichever needs it first.
|
|
152
156
|
let securityModule: Promise<typeof import("../../lib/crypto/messageSecurity.js")> | undefined;
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
// A few at a time: each is a download and a public-key decryption.
|
|
158
|
+
const entries = await mapWithConcurrency(
|
|
159
|
+
encrypted,
|
|
160
|
+
DECRYPT_CONCURRENCY,
|
|
161
|
+
async (message): Promise<[string, DecryptedRow] | null> => {
|
|
155
162
|
try {
|
|
156
163
|
const rawMime = await getMessageRawContent(message.uid);
|
|
157
164
|
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
@@ -159,15 +166,17 @@ async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys)
|
|
|
159
166
|
const { evaluateMessageSecurity } = await (securityModule ??= import("../../lib/crypto/messageSecurity.js"));
|
|
160
167
|
const security = await evaluateMessageSecurity(rawMime, unlocked);
|
|
161
168
|
if (!security.subject && !security.html) {
|
|
169
|
+
markDecryptFailed(message.uid);
|
|
162
170
|
return null;
|
|
163
171
|
}
|
|
164
172
|
const preview = security.html ? stripHtmlToText(security.html).slice(0, LIST_PREVIEW_MAX_LENGTH) : undefined;
|
|
165
173
|
rememberDecrypted(message.uid, message.mailboxUid, { subject: security.subject, preview });
|
|
166
174
|
return [message.uid, { subject: security.subject, preview }];
|
|
167
175
|
} catch {
|
|
176
|
+
markDecryptFailed(message.uid);
|
|
168
177
|
return null;
|
|
169
178
|
}
|
|
170
|
-
}
|
|
179
|
+
},
|
|
171
180
|
);
|
|
172
181
|
const result: Record<string, DecryptedRow> = {};
|
|
173
182
|
for (const entry of entries) {
|
|
@@ -470,6 +479,13 @@ function appendUnseenRows<T>(shown: T[], more: T[], idOf: (row: T) => string, ca
|
|
|
470
479
|
const messageUid = (message: Message) => message.uid;
|
|
471
480
|
const conversationKey = conversationRowKey;
|
|
472
481
|
|
|
482
|
+
/** How many conversations' messages are requested at once when many are ticked together (Select all on a long list). */
|
|
483
|
+
const RESOLVE_CONVERSATIONS_AT_ONCE = 5;
|
|
484
|
+
|
|
485
|
+
/** What of a conversation's summary changes when one of its messages does: a different one means the messages fetched for it are out of date. */
|
|
486
|
+
const conversationSignature = (row: ListedConversation) =>
|
|
487
|
+
[row.messageUids.join(","), row.unreadCount, row.flagged, row.folderUids.join(","), row.latestFolderUid].join("|");
|
|
488
|
+
|
|
473
489
|
/** Flattens and sorts a per-mailbox fetch into one merged, newest-first list - the aggregate ("All
|
|
474
490
|
* Inboxes" etc.) equivalent of `mergeSearchResults()` above, but simpler: an aggregated message has no
|
|
475
491
|
* natural relevance score to normalize, so this only ever sorts by `receivedDate`. */
|
|
@@ -482,7 +498,9 @@ function mergeInboxMessages(perMailbox: { mailbox: Mailbox; messages: Message[]
|
|
|
482
498
|
/**
|
|
483
499
|
* Fans out one `listMessages()` call per accessible mailbox that has a folder of `type`, merges the
|
|
484
500
|
* results newest-first. A mailbox with no matching folder, or whose fetch fails, simply contributes
|
|
485
|
-
* nothing - one mailbox's absence/failure must not blank out every other mailbox's messages.
|
|
501
|
+
* nothing - one mailbox's absence/failure must not blank out every other mailbox's messages. `listed`, the
|
|
502
|
+
* rows on screen when this is a refresh of them, makes a mailbox whose fetch fails keep the rows it has there
|
|
503
|
+
* instead of contributing none (a timeout must not make its mail vanish until the next success).
|
|
486
504
|
*
|
|
487
505
|
* **Pagination scope trim (deliberate, matching this file's own documented Tier 2/3 tradeoffs)**: there is
|
|
488
506
|
* no composite cursor across an arbitrary number of independently-paginated mailboxes in this pass - this
|
|
@@ -496,6 +514,7 @@ async function fetchAggregateMessages(
|
|
|
496
514
|
type: AggregateFolderType,
|
|
497
515
|
filter: MessageListFilter,
|
|
498
516
|
client?: ApiClient,
|
|
517
|
+
listed?: Message[],
|
|
499
518
|
): Promise<Message[]> {
|
|
500
519
|
const perMailbox = await Promise.all(
|
|
501
520
|
mailboxFolders.map(async ({ mailbox, folders }) => {
|
|
@@ -507,7 +526,7 @@ async function fetchAggregateMessages(
|
|
|
507
526
|
// this function's own pagination scope trim) - each mailbox contributes its own newest page and
|
|
508
527
|
// they're merged newest-first, which a different sort key couldn't be made honest across an
|
|
509
528
|
// arbitrary number of independently-paged folders.
|
|
510
|
-
const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() =>
|
|
529
|
+
const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => (listed ?? []).filter((m) => m.mailboxUid === mailbox.uid));
|
|
511
530
|
return { mailbox, messages };
|
|
512
531
|
}),
|
|
513
532
|
);
|
|
@@ -518,13 +537,15 @@ async function fetchAggregateMessages(
|
|
|
518
537
|
* The conversation-list counterpart of `fetchAggregateMessages()`: one `listConversations()` per accessible mailbox that has a folder of
|
|
519
538
|
* `type`, each scoped to *that folder* (so "All Inboxes" groups only Inbox mail - not the Sent Items copy of a reply, which a mailbox-wide
|
|
520
539
|
* grouping pulls in) and each row tagged with its mailbox, then merged newest activity first. Same first-page-only scope, and the same
|
|
521
|
-
* rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest
|
|
540
|
+
* rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest - or, with `listed` (the rows on
|
|
541
|
+
* screen, for a refresh of them), keeps the rows it has there.
|
|
522
542
|
*/
|
|
523
543
|
async function fetchAggregateConversations(
|
|
524
544
|
mailboxFolders: MailboxFolders[],
|
|
525
545
|
type: AggregateFolderType,
|
|
526
546
|
params: ConversationListParams,
|
|
527
547
|
client?: ApiClient,
|
|
548
|
+
listed?: ListedConversation[],
|
|
528
549
|
): Promise<ListedConversation[]> {
|
|
529
550
|
const perMailbox = await Promise.all(
|
|
530
551
|
mailboxFolders.map(async ({ mailbox, folders }): Promise<ListedConversation[]> => {
|
|
@@ -532,8 +553,8 @@ async function fetchAggregateConversations(
|
|
|
532
553
|
if (!folder) {
|
|
533
554
|
return [];
|
|
534
555
|
}
|
|
535
|
-
const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() =>
|
|
536
|
-
return rows.map((row) => ({ ...row, mailboxUid: mailbox.uid }));
|
|
556
|
+
const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => null);
|
|
557
|
+
return rows ? rows.map((row) => ({ ...row, mailboxUid: mailbox.uid })) : (listed ?? []).filter((row) => row.mailboxUid === mailbox.uid);
|
|
537
558
|
}),
|
|
538
559
|
);
|
|
539
560
|
return perMailbox.flat().sort((a, b) => Date.parse(b.latestDate) - Date.parse(a.latestDate));
|
|
@@ -805,6 +826,8 @@ function InboxContent({ userUid }: { userUid?: string }) {
|
|
|
805
826
|
messagesRef.current = messages;
|
|
806
827
|
const conversationsRef = useRef(conversations);
|
|
807
828
|
conversationsRef.current = conversations;
|
|
829
|
+
const selectedConversationIdsRef = useRef(selectedConversationIds);
|
|
830
|
+
selectedConversationIdsRef.current = selectedConversationIds;
|
|
808
831
|
// Reset to a fresh Map at the start of every new search pass (see the search effect below) - see
|
|
809
832
|
// resolveHitsToMessages()'s own doc comment on why this needs to persist *within* one pass but not
|
|
810
833
|
// across passes (a stale `null` for a uid that's since become resolvable elsewhere must not stick).
|
|
@@ -1485,8 +1508,9 @@ function InboxContent({ userUid }: { userUid?: string }) {
|
|
|
1485
1508
|
try {
|
|
1486
1509
|
if (asConversations && aggregateFolderType) {
|
|
1487
1510
|
// No paging here either: the fresh merged first pages are the list.
|
|
1488
|
-
const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client);
|
|
1511
|
+
const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client, conversationsRef.current);
|
|
1489
1512
|
if (isCurrent()) {
|
|
1513
|
+
refreshConversationMessages(conversationsRef.current, fresh);
|
|
1490
1514
|
setConversations(fresh);
|
|
1491
1515
|
}
|
|
1492
1516
|
} else if (asConversations) {
|
|
@@ -1495,12 +1519,13 @@ function InboxContent({ userUid }: { userUid?: string }) {
|
|
|
1495
1519
|
return;
|
|
1496
1520
|
}
|
|
1497
1521
|
const merged = mergeFirstPage(conversationsRef.current, fresh, conversationKey, MESSAGE_PAGE_SIZE);
|
|
1522
|
+
refreshConversationMessages(conversationsRef.current, merged.rows);
|
|
1498
1523
|
setConversations(merged.rows);
|
|
1499
1524
|
listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
|
|
1500
1525
|
setHasMore(!merged.complete);
|
|
1501
1526
|
} else if (aggregateFolderType) {
|
|
1502
1527
|
// No paging here (see `fetchAggregateMessages()`): the fresh merged first pages are the list.
|
|
1503
|
-
const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client);
|
|
1528
|
+
const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client, messagesRef.current);
|
|
1504
1529
|
if (isCurrent()) {
|
|
1505
1530
|
setMessages(fresh);
|
|
1506
1531
|
}
|
|
@@ -1954,35 +1979,67 @@ function InboxContent({ userUid }: { userUid?: string }) {
|
|
|
1954
1979
|
});
|
|
1955
1980
|
}
|
|
1956
1981
|
|
|
1957
|
-
/** Loads (once) the messages behind each of `
|
|
1958
|
-
* every bulk action below acts on
|
|
1959
|
-
*
|
|
1960
|
-
|
|
1961
|
-
|
|
1982
|
+
/** Loads (once, or again with `refetch`) the messages behind each of `rows`, so a ticked conversation resolves to the messages
|
|
1983
|
+
* every bulk action below acts on - a few requests at a time (Select all on a long list is hundreds of rows). A conversation
|
|
1984
|
+
* that fails to load is unticked, with one pop-up saying why - better than acting on a part of it - and the rest stay ticked.
|
|
1985
|
+
* What arrives after the list was loaded again is dropped: those rows are gone. */
|
|
1986
|
+
async function resolveConversations(rows: ListedConversation[], refetch = false) {
|
|
1987
|
+
const missing = rows.filter((row) => refetch || !conversationMessagesById[conversationKey(row)]);
|
|
1962
1988
|
if (missing.length === 0) {
|
|
1963
1989
|
return;
|
|
1964
1990
|
}
|
|
1991
|
+
const listRun = searchRunIdRef.current;
|
|
1965
1992
|
setResolvingSelection((n) => n + 1);
|
|
1966
1993
|
try {
|
|
1967
|
-
const
|
|
1994
|
+
const outcomes = await settleWithLimit(missing, RESOLVE_CONVERSATIONS_AT_ONCE, (row) =>
|
|
1968
1995
|
// A row of the "All mailboxes" view names its own mailbox; any other list is the open mailbox's.
|
|
1969
|
-
|
|
1996
|
+
searchRunIdRef.current === listRun
|
|
1997
|
+
? listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)
|
|
1998
|
+
: Promise.reject(new Error("The list was loaded again")),
|
|
1970
1999
|
);
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2000
|
+
if (searchRunIdRef.current !== listRun) {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
const loaded: Record<string, Message[]> = {};
|
|
2004
|
+
const failed = new Set<string>();
|
|
2005
|
+
let firstFailure: unknown;
|
|
2006
|
+
outcomes.forEach((outcome, index) => {
|
|
2007
|
+
const key = conversationKey(missing[index]);
|
|
2008
|
+
if (outcome.status === "fulfilled") {
|
|
2009
|
+
loaded[key] = outcome.value;
|
|
2010
|
+
} else {
|
|
2011
|
+
failed.add(key);
|
|
2012
|
+
firstFailure ??= outcome.reason;
|
|
1978
2013
|
}
|
|
1979
|
-
return next;
|
|
1980
2014
|
});
|
|
2015
|
+
setConversationMessagesById((prev) => ({ ...prev, ...loaded }));
|
|
2016
|
+
if (failed.size > 0) {
|
|
2017
|
+
notifyApiError(firstFailure, "Couldn't load the messages in one of those conversations");
|
|
2018
|
+
setSelectedConversationIds((prev) => new Set([...prev].filter((key) => !failed.has(key))));
|
|
2019
|
+
}
|
|
1981
2020
|
} finally {
|
|
1982
2021
|
setResolvingSelection((n) => n - 1);
|
|
1983
2022
|
}
|
|
1984
2023
|
}
|
|
1985
2024
|
|
|
2025
|
+
/**
|
|
2026
|
+
* Called as a refresh of the list swaps in `after` for `before`: the messages kept for a conversation whose summary changed (a reply arrived, a
|
|
2027
|
+
* message was read or moved) are out of date - the next action on it would act on a list missing the new message, or send an old `version`. The
|
|
2028
|
+
* cached messages of one that is not ticked are dropped, to be fetched when next wanted; a ticked one's are fetched again now, the old ones standing
|
|
2029
|
+
* until they arrive.
|
|
2030
|
+
*/
|
|
2031
|
+
function refreshConversationMessages(before: ListedConversation[], after: ListedConversation[]) {
|
|
2032
|
+
const was = new Map(before.map((row) => [conversationKey(row), conversationSignature(row)]));
|
|
2033
|
+
const changed = after.filter((row) => was.has(conversationKey(row)) && was.get(conversationKey(row)) !== conversationSignature(row));
|
|
2034
|
+
if (changed.length === 0) {
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
const ticked = changed.filter((row) => selectedConversationIdsRef.current.has(conversationKey(row)));
|
|
2038
|
+
const stale = new Set(changed.filter((row) => !ticked.includes(row)).flatMap((row) => [conversationKey(row), row.conversationId]));
|
|
2039
|
+
setConversationMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([key]) => !stale.has(key))));
|
|
2040
|
+
void resolveConversations(ticked, true);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1986
2043
|
function toggleConversationSelected(conversation: ListedConversation) {
|
|
1987
2044
|
const id = conversationKey(conversation);
|
|
1988
2045
|
const ticking = !selectedConversationIds.has(id);
|
|
@@ -2251,7 +2308,7 @@ function InboxContent({ userUid }: { userUid?: string }) {
|
|
|
2251
2308
|
|
|
2252
2309
|
/** Empty folder: permanently deletes everything in the folder being listed (Deleted Items or Junk Email) after the reader has confirmed. */
|
|
2253
2310
|
async function emptyListedFolder(folder: EmptiableFolder, count: number | undefined) {
|
|
2254
|
-
const outcome = await permanent.requestEmptyFolder(
|
|
2311
|
+
const outcome = await permanent.requestEmptyFolder(folder, count);
|
|
2255
2312
|
if (!outcome) {
|
|
2256
2313
|
return;
|
|
2257
2314
|
}
|
|
@@ -2799,6 +2856,12 @@ function InboxContent({ userUid }: { userUid?: string }) {
|
|
|
2799
2856
|
uid: emptiableFolders[0].uid,
|
|
2800
2857
|
name: aggregateFolderType ? (emptiableFolders[0].type === "junk" ? "Junk Email" : "Deleted Items") : emptiableFolders[0].name,
|
|
2801
2858
|
uids: aggregateFolderType ? emptiableFolders.map((f) => f.uid) : undefined,
|
|
2859
|
+
// The mailboxes whose folders these are, for the confirmation to name (it empties each of them, a share included).
|
|
2860
|
+
scope: aggregateFolderType
|
|
2861
|
+
? mailboxFolders
|
|
2862
|
+
.filter((entry) => entry.folders.some((f) => f.type === aggregateFolderType))
|
|
2863
|
+
.map(({ mailbox }) => `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}`)
|
|
2864
|
+
: undefined,
|
|
2802
2865
|
};
|
|
2803
2866
|
// How many it holds as the sidebar's counts have it (kept right as messages leave), unless that says none while the list shows some.
|
|
2804
2867
|
const emptiableTotal = emptiableFolders.reduce((sum, f) => sum + folderCountOf(f).total, 0);
|
|
@@ -40,12 +40,17 @@ function MessageDetailContent({ uid }: { uid: string }) {
|
|
|
40
40
|
const conversationId: string | null = useSearchParams()[0].get("conversation");
|
|
41
41
|
|
|
42
42
|
useEffect(() => {
|
|
43
|
+
// An answer for a message since replaced (the uid changed without the page remounting) must not be shown as the current one.
|
|
44
|
+
let cancelled = false;
|
|
43
45
|
setLoading(true);
|
|
44
46
|
setError(null);
|
|
45
47
|
getMessage(uid, client)
|
|
46
|
-
.then(setMessage)
|
|
47
|
-
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
|
|
48
|
-
.finally(() => setLoading(false));
|
|
48
|
+
.then((loaded) => !cancelled && setMessage(loaded))
|
|
49
|
+
.catch((err) => !cancelled && setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
|
|
50
|
+
.finally(() => !cancelled && setLoading(false));
|
|
51
|
+
return () => {
|
|
52
|
+
cancelled = true;
|
|
53
|
+
};
|
|
49
54
|
}, [uid, client]);
|
|
50
55
|
|
|
51
56
|
// See `apps/www/index.tsx`'s identical effect's own doc comment - a failure here just hides the
|
|
@@ -9,6 +9,7 @@ import { updateMailbox } from "../../../../lib/mail/mailApi.js";
|
|
|
9
9
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
10
|
import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
11
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
12
|
+
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
12
13
|
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
13
14
|
|
|
14
15
|
const INPUT_CLASS =
|
|
@@ -41,10 +42,21 @@ function AutoReplyContent() {
|
|
|
41
42
|
const [version, setVersion] = useState(mailbox.version);
|
|
42
43
|
const [saving, setSaving] = useState(false);
|
|
43
44
|
const [saved, setSaved] = useState(false);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
46
|
|
|
45
47
|
async function handleSubmit(e: FormEvent) {
|
|
46
48
|
e.preventDefault();
|
|
47
49
|
setSaved(false);
|
|
50
|
+
// Turned on with nothing to send, or over a window that ends before it starts, would answer people with an empty message or never at all.
|
|
51
|
+
if (oofEnabled && !oofMessage.trim()) {
|
|
52
|
+
setError("Write the message to send while automatic replies are on.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (oofEnabled && oofStartTime && oofEndTime && new Date(oofEndTime) <= new Date(oofStartTime)) {
|
|
56
|
+
setError("The end must be after the start.");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setError(null);
|
|
48
60
|
setSaving(true);
|
|
49
61
|
try {
|
|
50
62
|
const updated = await updateMailbox({
|
|
@@ -75,6 +87,7 @@ function AutoReplyContent() {
|
|
|
75
87
|
</p>
|
|
76
88
|
|
|
77
89
|
{saved && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
|
|
90
|
+
{error && <Alert>{error}</Alert>}
|
|
78
91
|
|
|
79
92
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
80
93
|
<label className="flex items-center gap-2 text-sm font-medium">
|
|
@@ -93,6 +93,10 @@ function ListSection({ kind, entries, otherEntries, ownAddresses, editable, onAd
|
|
|
93
93
|
setError("That is one of your own addresses, so it cannot be blocked.");
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
+
if (kind === "blocked" && checked.entry.startsWith("@") && ownAddresses.some((address) => address.toLowerCase().endsWith(checked.entry))) {
|
|
97
|
+
setError("That is your own domain: blocking it would send the mail of everyone at it - yours included - to Junk.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
96
100
|
setError(null);
|
|
97
101
|
setBusy(true);
|
|
98
102
|
try {
|