@rapidmx/web-client 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +3 -3
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +2 -2
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +31 -2
- package/dist/lib/admin/pluginsApi.js +23 -1
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +45 -3
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -9,12 +9,13 @@ import { useEffect, useState } from "react";
|
|
|
9
9
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
10
10
|
import { deleteContact, getContact } from "../../../lib/contacts/contactsApi.js";
|
|
11
11
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
12
|
-
import ContactsShell from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
12
|
+
import ContactsShell, { useContactsShell } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
13
13
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
14
14
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
15
15
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
16
16
|
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
17
17
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
18
|
+
import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
|
|
18
19
|
/**
|
|
19
20
|
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
20
21
|
* existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
|
|
@@ -31,6 +32,9 @@ function ContactDetailContent({ uid }) {
|
|
|
31
32
|
const [loading, setLoading] = useState(true);
|
|
32
33
|
const [error, setError] = useState(null);
|
|
33
34
|
const [mode, setMode] = useState("view");
|
|
35
|
+
const { mailboxes } = useContactsShell();
|
|
36
|
+
// A contact in a mailbox shared with the reader view-only can be read, not changed.
|
|
37
|
+
const writable = useMailboxUpdateAccess(mailboxes.find((mailbox) => mailbox.uid === contact?.mailboxUid));
|
|
34
38
|
useEffect(() => {
|
|
35
39
|
setLoading(true);
|
|
36
40
|
setError(null);
|
|
@@ -72,7 +76,7 @@ function ContactDetailContent({ uid }) {
|
|
|
72
76
|
setMode("view");
|
|
73
77
|
}, onCancel: () => setMode("view") })] }));
|
|
74
78
|
}
|
|
75
|
-
return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref, onKeysChanged: handleKeysChanged, onChanged: setContact }) }));
|
|
79
|
+
return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), canWrite: writable, backHref: backHref, onKeysChanged: handleKeysChanged, onChanged: setContact }) }));
|
|
76
80
|
}
|
|
77
81
|
export default ContactDetailPage;
|
|
78
82
|
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
|
@@ -7,8 +7,9 @@ import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
|
7
7
|
import { useNavigate } from "../../shared/navigation/index.js";
|
|
8
8
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
10
|
-
import { createContact, deleteContact, listContacts, listDeletedContacts, setContactFavorite,
|
|
11
|
-
import { contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
10
|
+
import { createContact, deleteContact, listContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
|
|
11
|
+
import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
12
|
+
import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
|
|
12
13
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
13
14
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
14
15
|
import { useCompose } from "../../shared/components/mail/compose/ComposeContext.js";
|
|
@@ -17,11 +18,12 @@ import ContactsSidebar from "../../shared/components/contacts/ContactsSidebar.js
|
|
|
17
18
|
import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js";
|
|
18
19
|
import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
|
|
19
20
|
import { HiOutlineUserPlus } from "react-icons/hi2";
|
|
20
|
-
import
|
|
21
|
+
import ContactPhotoAvatar from "../../../lib/components/avatar/ContactPhotoAvatar.js";
|
|
21
22
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
22
23
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
23
24
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
24
25
|
import { getMyMailboxAccess } from "../../../lib/mail/mailboxAccessApi.js";
|
|
26
|
+
import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
|
|
25
27
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
26
28
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
27
29
|
import Modal from "../../../lib/components/overlays/Modal.js";
|
|
@@ -30,6 +32,7 @@ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigne
|
|
|
30
32
|
import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
31
33
|
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
32
34
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
35
|
+
import { notify } from "../../shared/notifications/store.js";
|
|
33
36
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
34
37
|
function ContactsPage(props) {
|
|
35
38
|
return (_jsx(ContactsShell, { ...props, children: _jsx(ContactsContent, { userUid: props.userUid }) }));
|
|
@@ -57,6 +60,8 @@ function ContactsContent({ userUid }) {
|
|
|
57
60
|
const { folderUid, mailboxUid, mailboxes } = useContactsShell();
|
|
58
61
|
// The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
|
|
59
62
|
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
|
|
63
|
+
// A mailbox shared with the reader view-only: its contacts can be read, not changed.
|
|
64
|
+
const mailboxWritable = useMailboxUpdateAccess(mailboxes.find((mb) => mb.uid === mailboxUid));
|
|
60
65
|
const { openCompose } = useCompose();
|
|
61
66
|
const isMobile = useIsMobile();
|
|
62
67
|
const navigate = useNavigate();
|
|
@@ -263,6 +268,22 @@ function ContactsContent({ userUid }) {
|
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
// The bulk handlers below try every contact and report each failure: the same failure repeated is one pop-up with a count.
|
|
271
|
+
// 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.
|
|
272
|
+
const bulkRunning = useRef(false);
|
|
273
|
+
function singleFlight(run) {
|
|
274
|
+
return async () => {
|
|
275
|
+
if (bulkRunning.current) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
bulkRunning.current = true;
|
|
279
|
+
try {
|
|
280
|
+
await run();
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
bulkRunning.current = false;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
266
287
|
async function handleBulkDelete() {
|
|
267
288
|
setConfirmingBulkDelete(false);
|
|
268
289
|
for (const contact of checkedContacts) {
|
|
@@ -291,7 +312,7 @@ function ContactsContent({ userUid }) {
|
|
|
291
312
|
// contacts happen to be open - the compose window's From field can still change it.
|
|
292
313
|
openCompose({ to: addresses.join(", ") });
|
|
293
314
|
}
|
|
294
|
-
|
|
315
|
+
const handleToggleFavorite = singleFlight(async () => {
|
|
295
316
|
const allFavorited = checkedContacts.every((c) => c.favorite);
|
|
296
317
|
for (const contact of checkedContacts) {
|
|
297
318
|
try {
|
|
@@ -303,8 +324,8 @@ function ContactsContent({ userUid }) {
|
|
|
303
324
|
}
|
|
304
325
|
clearPinnedSignerCache();
|
|
305
326
|
await reload();
|
|
306
|
-
}
|
|
307
|
-
|
|
327
|
+
});
|
|
328
|
+
const handleAddCategory = singleFlight(async () => {
|
|
308
329
|
const category = window.prompt("Category name");
|
|
309
330
|
if (!category?.trim()) {
|
|
310
331
|
return;
|
|
@@ -322,7 +343,7 @@ function ContactsContent({ userUid }) {
|
|
|
322
343
|
}
|
|
323
344
|
clearPinnedSignerCache();
|
|
324
345
|
await reload();
|
|
325
|
-
}
|
|
346
|
+
});
|
|
326
347
|
function handleExportVCard() {
|
|
327
348
|
downloadTextFile(checkedContacts.length === 1 ? `${checkedContacts[0].displayName}.vcf` : "contacts.vcf", checkedContacts.length === 1 ? contactToVCard(checkedContacts[0]) : contactsToVCardFile(checkedContacts));
|
|
328
349
|
}
|
|
@@ -330,9 +351,37 @@ function ContactsContent({ userUid }) {
|
|
|
330
351
|
if (!mailboxUid || !folderUid) {
|
|
331
352
|
return;
|
|
332
353
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
354
|
+
// 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
|
|
355
|
+
// lock the page for minutes and flood the server. Whatever goes wrong while reading it is said, not left as an unhandled rejection.
|
|
356
|
+
if (file.size > VCARD_MAX_CHARS) {
|
|
357
|
+
notify({ kind: "error", title: "Couldn't import contacts", message: `That file is larger than ${Math.round(VCARD_MAX_CHARS / 1000000)} MB. Split it into smaller files.` });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
let parsed;
|
|
361
|
+
try {
|
|
362
|
+
parsed = parseVCards(await file.text());
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
notifyApiError(err, "Couldn't read that contacts file");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (parsed.length >= VCARD_MAX_CONTACTS) {
|
|
369
|
+
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.` });
|
|
370
|
+
}
|
|
371
|
+
// 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
|
|
372
|
+
// 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.
|
|
373
|
+
let fresh;
|
|
374
|
+
try {
|
|
375
|
+
fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
notifyApiError(err, "Couldn't check the file against your contacts");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (fresh.length < parsed.length) {
|
|
382
|
+
notify({ kind: "info", title: "Some contacts were skipped", message: `${parsed.length - fresh.length} of the contacts in that file are already in your contacts.` });
|
|
383
|
+
}
|
|
384
|
+
for (const input of fresh) {
|
|
336
385
|
try {
|
|
337
386
|
await createContact({ mailboxUid, folderUid, ...input }, client);
|
|
338
387
|
}
|
|
@@ -346,7 +395,9 @@ function ContactsContent({ userUid }) {
|
|
|
346
395
|
return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row md:flex-none md:h-[calc(100dvh_-_var(--rr-header-h,4rem))] md:overflow-hidden", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted }), _jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex", children: [_jsxs("div", { className: [
|
|
347
396
|
"w-full md:w-[clamp(16rem,32vw,26rem)] min-w-0 md:shrink-0 md:border-r border-border md:flex flex-col min-h-0",
|
|
348
397
|
mode === "new" ? "hidden md:flex" : "flex",
|
|
349
|
-
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
|
|
398
|
+
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
|
|
399
|
+
// A deleted contact has no pane to show (it can't be restored or opened), and its route would answer 404.
|
|
400
|
+
disabled: isDeletedView, onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left disabled:cursor-default", children: [_jsx(ContactPhotoAvatar, { lazy: true, displayName: contact.displayName, size: 28, contact: contact, email: contact.emails[0]?.address }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), canWrite: mailboxWritable, onKeysChanged: () => void reload(), onChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
|
|
350
401
|
}
|
|
351
402
|
export default ContactsPage;
|
|
352
403
|
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
package/dist/apps/www/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { searchLocalIndex } from "../shared/search/searchTier2.js";
|
|
|
21
21
|
import { SEARCH_LIMITS, classifyFailure, describeFailures, hitKey, mergeSearchResults, runLimited, searchPageSize, tagHits, withTimeout, } from "../shared/search/crossMailboxSearch.js";
|
|
22
22
|
import { getUnlockedKeys, subscribeKeySession } from "../../lib/crypto/keySession.js";
|
|
23
23
|
import { useMessageAttachments } from "../../lib/mail/mailDetailHooks.js";
|
|
24
|
+
import { settleWithLimit } from "../../lib/util/settleWithLimit.js";
|
|
24
25
|
import useIsMobile from "../../lib/util/useIsMobile.js";
|
|
25
26
|
import MailShell, { useMailShell, } from "../shared/components/mail/layout/MailShell.js";
|
|
26
27
|
import MailAddress from "../shared/components/mail/MailAddress.js";
|
|
@@ -34,7 +35,7 @@ import { setReadStateMany } from "../shared/mail/messageReadState.js";
|
|
|
34
35
|
import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
|
|
35
36
|
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
|
|
36
37
|
import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
|
|
37
|
-
import { rememberDecrypted } from "../shared/mail/decryptedMessages.js";
|
|
38
|
+
import { decryptFailedBefore, mapWithConcurrency, markDecryptFailed, rememberDecrypted } from "../shared/mail/decryptedMessages.js";
|
|
38
39
|
import ConversationList, { conversationRowKey } from "../shared/components/mail/ConversationList.js";
|
|
39
40
|
import SwipeRow from "../shared/components/mail/SwipeRow.js";
|
|
40
41
|
import InviteRowChip from "../shared/components/mail/invite/InviteRowChip.js";
|
|
@@ -57,6 +58,8 @@ import { notifyApiError } from "../shared/notifications/apiErrors.js";
|
|
|
57
58
|
const MESSAGE_PAGE_SIZE = 50;
|
|
58
59
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
59
60
|
const LIST_PREVIEW_MAX_LENGTH = 160;
|
|
61
|
+
/** How many encrypted rows are fetched and decrypted at once. */
|
|
62
|
+
const DECRYPT_CONCURRENCY = 4;
|
|
60
63
|
/** The literal outer-envelope `Subject` every encrypted message carries server-side - RFC 9788's
|
|
61
64
|
* `hcp_baseline` policy obscures it to this exact string (see `smimeMessage.ts`'s
|
|
62
65
|
* `applyBaselineOuterHeaders()`), which is also all `Message.subject` ever shows for one of these until
|
|
@@ -90,10 +93,12 @@ function stripHtmlToText(html) {
|
|
|
90
93
|
* and `handleUnlockList()`), and a single row's fetch/decrypt failure never blocks the rest.
|
|
91
94
|
*/
|
|
92
95
|
async function decryptEncryptedRows(messages, unlocked) {
|
|
93
|
-
|
|
96
|
+
// A row that already came to nothing since the last unlock is not downloaded again by every refresh of the list.
|
|
97
|
+
const encrypted = messages.filter((m) => m.subject === ENCRYPTED_SUBJECT_PLACEHOLDER && !decryptFailedBefore(m.uid));
|
|
94
98
|
// Imported once for all the rows, by whichever needs it first.
|
|
95
99
|
let securityModule;
|
|
96
|
-
|
|
100
|
+
// A few at a time: each is a download and a public-key decryption.
|
|
101
|
+
const entries = await mapWithConcurrency(encrypted, DECRYPT_CONCURRENCY, async (message) => {
|
|
97
102
|
try {
|
|
98
103
|
const rawMime = await getMessageRawContent(message.uid);
|
|
99
104
|
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
@@ -101,6 +106,7 @@ async function decryptEncryptedRows(messages, unlocked) {
|
|
|
101
106
|
const { evaluateMessageSecurity } = await (securityModule ?? (securityModule = import("../../lib/crypto/messageSecurity.js")));
|
|
102
107
|
const security = await evaluateMessageSecurity(rawMime, unlocked);
|
|
103
108
|
if (!security.subject && !security.html) {
|
|
109
|
+
markDecryptFailed(message.uid);
|
|
104
110
|
return null;
|
|
105
111
|
}
|
|
106
112
|
const preview = security.html ? stripHtmlToText(security.html).slice(0, LIST_PREVIEW_MAX_LENGTH) : undefined;
|
|
@@ -108,9 +114,10 @@ async function decryptEncryptedRows(messages, unlocked) {
|
|
|
108
114
|
return [message.uid, { subject: security.subject, preview }];
|
|
109
115
|
}
|
|
110
116
|
catch {
|
|
117
|
+
markDecryptFailed(message.uid);
|
|
111
118
|
return null;
|
|
112
119
|
}
|
|
113
|
-
})
|
|
120
|
+
});
|
|
114
121
|
const result = {};
|
|
115
122
|
for (const entry of entries) {
|
|
116
123
|
if (entry) {
|
|
@@ -319,6 +326,10 @@ function appendUnseenRows(shown, more, idOf, cap) {
|
|
|
319
326
|
}
|
|
320
327
|
const messageUid = (message) => message.uid;
|
|
321
328
|
const conversationKey = conversationRowKey;
|
|
329
|
+
/** How many conversations' messages are requested at once when many are ticked together (Select all on a long list). */
|
|
330
|
+
const RESOLVE_CONVERSATIONS_AT_ONCE = 5;
|
|
331
|
+
/** 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. */
|
|
332
|
+
const conversationSignature = (row) => [row.messageUids.join(","), row.unreadCount, row.flagged, row.folderUids.join(","), row.latestFolderUid].join("|");
|
|
322
333
|
/** Flattens and sorts a per-mailbox fetch into one merged, newest-first list - the aggregate ("All
|
|
323
334
|
* Inboxes" etc.) equivalent of `mergeSearchResults()` above, but simpler: an aggregated message has no
|
|
324
335
|
* natural relevance score to normalize, so this only ever sorts by `receivedDate`. */
|
|
@@ -330,7 +341,9 @@ function mergeInboxMessages(perMailbox) {
|
|
|
330
341
|
/**
|
|
331
342
|
* Fans out one `listMessages()` call per accessible mailbox that has a folder of `type`, merges the
|
|
332
343
|
* results newest-first. A mailbox with no matching folder, or whose fetch fails, simply contributes
|
|
333
|
-
* nothing - one mailbox's absence/failure must not blank out every other mailbox's messages.
|
|
344
|
+
* nothing - one mailbox's absence/failure must not blank out every other mailbox's messages. `listed`, the
|
|
345
|
+
* rows on screen when this is a refresh of them, makes a mailbox whose fetch fails keep the rows it has there
|
|
346
|
+
* instead of contributing none (a timeout must not make its mail vanish until the next success).
|
|
334
347
|
*
|
|
335
348
|
* **Pagination scope trim (deliberate, matching this file's own documented Tier 2/3 tradeoffs)**: there is
|
|
336
349
|
* no composite cursor across an arbitrary number of independently-paginated mailboxes in this pass - this
|
|
@@ -339,7 +352,7 @@ function mergeInboxMessages(perMailbox) {
|
|
|
339
352
|
* composite-cursor "load more" per mailbox is a natural v2 if usage shows people scrolling past the first
|
|
340
353
|
* page in aggregate view often.
|
|
341
354
|
*/
|
|
342
|
-
async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
|
|
355
|
+
async function fetchAggregateMessages(mailboxFolders, type, filter, client, listed) {
|
|
343
356
|
const perMailbox = await Promise.all(mailboxFolders.map(async ({ mailbox, folders }) => {
|
|
344
357
|
const folder = folders.find((f) => f.type === type);
|
|
345
358
|
if (!folder) {
|
|
@@ -349,7 +362,7 @@ async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
|
|
|
349
362
|
// this function's own pagination scope trim) - each mailbox contributes its own newest page and
|
|
350
363
|
// they're merged newest-first, which a different sort key couldn't be made honest across an
|
|
351
364
|
// arbitrary number of independently-paged folders.
|
|
352
|
-
const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => []);
|
|
365
|
+
const messages = await listMessages(folder.uid, { limit: MESSAGE_PAGE_SIZE, filter }, client).catch(() => (listed ?? []).filter((m) => m.mailboxUid === mailbox.uid));
|
|
353
366
|
return { mailbox, messages };
|
|
354
367
|
}));
|
|
355
368
|
return mergeInboxMessages(perMailbox);
|
|
@@ -358,16 +371,17 @@ async function fetchAggregateMessages(mailboxFolders, type, filter, client) {
|
|
|
358
371
|
* The conversation-list counterpart of `fetchAggregateMessages()`: one `listConversations()` per accessible mailbox that has a folder of
|
|
359
372
|
* `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
|
|
360
373
|
* grouping pulls in) and each row tagged with its mailbox, then merged newest activity first. Same first-page-only scope, and the same
|
|
361
|
-
* rule that a mailbox that has no such folder or fails to answer contributes nothing rather than blanking the rest
|
|
374
|
+
* 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
|
|
375
|
+
* screen, for a refresh of them), keeps the rows it has there.
|
|
362
376
|
*/
|
|
363
|
-
async function fetchAggregateConversations(mailboxFolders, type, params, client) {
|
|
377
|
+
async function fetchAggregateConversations(mailboxFolders, type, params, client, listed) {
|
|
364
378
|
const perMailbox = await Promise.all(mailboxFolders.map(async ({ mailbox, folders }) => {
|
|
365
379
|
const folder = folders.find((f) => f.type === type);
|
|
366
380
|
if (!folder) {
|
|
367
381
|
return [];
|
|
368
382
|
}
|
|
369
|
-
const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() =>
|
|
370
|
-
return rows.map((row) => ({ ...row, mailboxUid: mailbox.uid }));
|
|
383
|
+
const rows = await listConversations(mailbox.uid, { ...params, folderUid: folder.uid, page: 0 }, client).catch(() => null);
|
|
384
|
+
return rows ? rows.map((row) => ({ ...row, mailboxUid: mailbox.uid })) : (listed ?? []).filter((row) => row.mailboxUid === mailbox.uid);
|
|
371
385
|
}));
|
|
372
386
|
return perMailbox.flat().sort((a, b) => Date.parse(b.latestDate) - Date.parse(a.latestDate));
|
|
373
387
|
}
|
|
@@ -621,6 +635,8 @@ function InboxContent({ userUid }) {
|
|
|
621
635
|
messagesRef.current = messages;
|
|
622
636
|
const conversationsRef = useRef(conversations);
|
|
623
637
|
conversationsRef.current = conversations;
|
|
638
|
+
const selectedConversationIdsRef = useRef(selectedConversationIds);
|
|
639
|
+
selectedConversationIdsRef.current = selectedConversationIds;
|
|
624
640
|
// Reset to a fresh Map at the start of every new search pass (see the search effect below) - see
|
|
625
641
|
// resolveHitsToMessages()'s own doc comment on why this needs to persist *within* one pass but not
|
|
626
642
|
// across passes (a stale `null` for a uid that's since become resolvable elsewhere must not stick).
|
|
@@ -1256,8 +1272,9 @@ function InboxContent({ userUid }) {
|
|
|
1256
1272
|
try {
|
|
1257
1273
|
if (asConversations && aggregateFolderType) {
|
|
1258
1274
|
// No paging here either: the fresh merged first pages are the list.
|
|
1259
|
-
const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client);
|
|
1275
|
+
const fresh = await fetchAggregateConversations(mailboxFolders, aggregateFolderType, conversationParams(0), client, conversationsRef.current);
|
|
1260
1276
|
if (isCurrent()) {
|
|
1277
|
+
refreshConversationMessages(conversationsRef.current, fresh);
|
|
1261
1278
|
setConversations(fresh);
|
|
1262
1279
|
}
|
|
1263
1280
|
}
|
|
@@ -1267,13 +1284,14 @@ function InboxContent({ userUid }) {
|
|
|
1267
1284
|
return;
|
|
1268
1285
|
}
|
|
1269
1286
|
const merged = mergeFirstPage(conversationsRef.current, fresh, conversationKey, MESSAGE_PAGE_SIZE);
|
|
1287
|
+
refreshConversationMessages(conversationsRef.current, merged.rows);
|
|
1270
1288
|
setConversations(merged.rows);
|
|
1271
1289
|
listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
|
|
1272
1290
|
setHasMore(!merged.complete);
|
|
1273
1291
|
}
|
|
1274
1292
|
else if (aggregateFolderType) {
|
|
1275
1293
|
// No paging here (see `fetchAggregateMessages()`): the fresh merged first pages are the list.
|
|
1276
|
-
const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client);
|
|
1294
|
+
const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter, client, messagesRef.current);
|
|
1277
1295
|
if (isCurrent()) {
|
|
1278
1296
|
setMessages(fresh);
|
|
1279
1297
|
}
|
|
@@ -1685,35 +1703,66 @@ function InboxContent({ userUid }) {
|
|
|
1685
1703
|
return next;
|
|
1686
1704
|
});
|
|
1687
1705
|
}
|
|
1688
|
-
/** Loads (once) the messages behind each of `
|
|
1689
|
-
* every bulk action below acts on
|
|
1690
|
-
*
|
|
1691
|
-
|
|
1692
|
-
|
|
1706
|
+
/** Loads (once, or again with `refetch`) the messages behind each of `rows`, so a ticked conversation resolves to the messages
|
|
1707
|
+
* every bulk action below acts on - a few requests at a time (Select all on a long list is hundreds of rows). A conversation
|
|
1708
|
+
* 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.
|
|
1709
|
+
* What arrives after the list was loaded again is dropped: those rows are gone. */
|
|
1710
|
+
async function resolveConversations(rows, refetch = false) {
|
|
1711
|
+
const missing = rows.filter((row) => refetch || !conversationMessagesById[conversationKey(row)]);
|
|
1693
1712
|
if (missing.length === 0) {
|
|
1694
1713
|
return;
|
|
1695
1714
|
}
|
|
1715
|
+
const listRun = searchRunIdRef.current;
|
|
1696
1716
|
setResolvingSelection((n) => n + 1);
|
|
1697
1717
|
try {
|
|
1698
|
-
const
|
|
1718
|
+
const outcomes = await settleWithLimit(missing, RESOLVE_CONVERSATIONS_AT_ONCE, (row) =>
|
|
1699
1719
|
// A row of the "All mailboxes" view names its own mailbox; any other list is the open mailbox's.
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1720
|
+
searchRunIdRef.current === listRun
|
|
1721
|
+
? listConversationMessages(row.mailboxUid ?? activeMailboxUid, row.conversationId, {}, client)
|
|
1722
|
+
: Promise.reject(new Error("The list was loaded again")));
|
|
1723
|
+
if (searchRunIdRef.current !== listRun) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const loaded = {};
|
|
1727
|
+
const failed = new Set();
|
|
1728
|
+
let firstFailure;
|
|
1729
|
+
outcomes.forEach((outcome, index) => {
|
|
1730
|
+
const key = conversationKey(missing[index]);
|
|
1731
|
+
if (outcome.status === "fulfilled") {
|
|
1732
|
+
loaded[key] = outcome.value;
|
|
1733
|
+
}
|
|
1734
|
+
else {
|
|
1735
|
+
failed.add(key);
|
|
1736
|
+
firstFailure ?? (firstFailure = outcome.reason);
|
|
1709
1737
|
}
|
|
1710
|
-
return next;
|
|
1711
1738
|
});
|
|
1739
|
+
setConversationMessagesById((prev) => ({ ...prev, ...loaded }));
|
|
1740
|
+
if (failed.size > 0) {
|
|
1741
|
+
notifyApiError(firstFailure, "Couldn't load the messages in one of those conversations");
|
|
1742
|
+
setSelectedConversationIds((prev) => new Set([...prev].filter((key) => !failed.has(key))));
|
|
1743
|
+
}
|
|
1712
1744
|
}
|
|
1713
1745
|
finally {
|
|
1714
1746
|
setResolvingSelection((n) => n - 1);
|
|
1715
1747
|
}
|
|
1716
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* 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
|
|
1751
|
+
* 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
|
|
1752
|
+
* 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
|
|
1753
|
+
* until they arrive.
|
|
1754
|
+
*/
|
|
1755
|
+
function refreshConversationMessages(before, after) {
|
|
1756
|
+
const was = new Map(before.map((row) => [conversationKey(row), conversationSignature(row)]));
|
|
1757
|
+
const changed = after.filter((row) => was.has(conversationKey(row)) && was.get(conversationKey(row)) !== conversationSignature(row));
|
|
1758
|
+
if (changed.length === 0) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const ticked = changed.filter((row) => selectedConversationIdsRef.current.has(conversationKey(row)));
|
|
1762
|
+
const stale = new Set(changed.filter((row) => !ticked.includes(row)).flatMap((row) => [conversationKey(row), row.conversationId]));
|
|
1763
|
+
setConversationMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([key]) => !stale.has(key))));
|
|
1764
|
+
void resolveConversations(ticked, true);
|
|
1765
|
+
}
|
|
1717
1766
|
function toggleConversationSelected(conversation) {
|
|
1718
1767
|
const id = conversationKey(conversation);
|
|
1719
1768
|
const ticking = !selectedConversationIds.has(id);
|
|
@@ -1955,7 +2004,7 @@ function InboxContent({ userUid }) {
|
|
|
1955
2004
|
}
|
|
1956
2005
|
/** Empty folder: permanently deletes everything in the folder being listed (Deleted Items or Junk Email) after the reader has confirmed. */
|
|
1957
2006
|
async function emptyListedFolder(folder, count) {
|
|
1958
|
-
const outcome = await permanent.requestEmptyFolder(
|
|
2007
|
+
const outcome = await permanent.requestEmptyFolder(folder, count);
|
|
1959
2008
|
if (!outcome) {
|
|
1960
2009
|
return;
|
|
1961
2010
|
}
|
|
@@ -2325,6 +2374,12 @@ function InboxContent({ userUid }) {
|
|
|
2325
2374
|
uid: emptiableFolders[0].uid,
|
|
2326
2375
|
name: aggregateFolderType ? (emptiableFolders[0].type === "junk" ? "Junk Email" : "Deleted Items") : emptiableFolders[0].name,
|
|
2327
2376
|
uids: aggregateFolderType ? emptiableFolders.map((f) => f.uid) : undefined,
|
|
2377
|
+
// The mailboxes whose folders these are, for the confirmation to name (it empties each of them, a share included).
|
|
2378
|
+
scope: aggregateFolderType
|
|
2379
|
+
? mailboxFolders
|
|
2380
|
+
.filter((entry) => entry.folders.some((f) => f.type === aggregateFolderType))
|
|
2381
|
+
.map(({ mailbox }) => `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}`)
|
|
2382
|
+
: undefined,
|
|
2328
2383
|
};
|
|
2329
2384
|
// 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.
|
|
2330
2385
|
const emptiableTotal = emptiableFolders.reduce((sum, f) => sum + folderCountOf(f).total, 0);
|
|
@@ -34,12 +34,17 @@ function MessageDetailContent({ uid }) {
|
|
|
34
34
|
const [labels, setLabels] = useState([]);
|
|
35
35
|
const conversationId = useSearchParams()[0].get("conversation");
|
|
36
36
|
useEffect(() => {
|
|
37
|
+
// An answer for a message since replaced (the uid changed without the page remounting) must not be shown as the current one.
|
|
38
|
+
let cancelled = false;
|
|
37
39
|
setLoading(true);
|
|
38
40
|
setError(null);
|
|
39
41
|
getMessage(uid, client)
|
|
40
|
-
.then(setMessage)
|
|
41
|
-
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
|
|
42
|
-
.finally(() => setLoading(false));
|
|
42
|
+
.then((loaded) => !cancelled && setMessage(loaded))
|
|
43
|
+
.catch((err) => !cancelled && setError(err instanceof ApiRequestError ? err.message : "Could not load this message."))
|
|
44
|
+
.finally(() => !cancelled && setLoading(false));
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true;
|
|
47
|
+
};
|
|
43
48
|
}, [uid, client]);
|
|
44
49
|
// See `apps/www/index.tsx`'s identical effect's own doc comment - a failure here just hides the
|
|
45
50
|
// Labels control rather than blocking the rest of the page. Keyed on the message's own mailbox, not the
|
|
@@ -10,6 +10,7 @@ import { updateMailbox } from "../../../../lib/mail/mailApi.js";
|
|
|
10
10
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
11
11
|
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
12
12
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
13
|
+
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
13
14
|
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
14
15
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
15
16
|
function SettingsAutoReplyPage(props) {
|
|
@@ -31,9 +32,20 @@ function AutoReplyContent() {
|
|
|
31
32
|
const [version, setVersion] = useState(mailbox.version);
|
|
32
33
|
const [saving, setSaving] = useState(false);
|
|
33
34
|
const [saved, setSaved] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
34
36
|
async function handleSubmit(e) {
|
|
35
37
|
e.preventDefault();
|
|
36
38
|
setSaved(false);
|
|
39
|
+
// 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.
|
|
40
|
+
if (oofEnabled && !oofMessage.trim()) {
|
|
41
|
+
setError("Write the message to send while automatic replies are on.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (oofEnabled && oofStartTime && oofEndTime && new Date(oofEndTime) <= new Date(oofStartTime)) {
|
|
45
|
+
setError("The end must be after the start.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setError(null);
|
|
37
49
|
setSaving(true);
|
|
38
50
|
try {
|
|
39
51
|
const updated = await updateMailbox({
|
|
@@ -55,7 +67,7 @@ function AutoReplyContent() {
|
|
|
55
67
|
setSaving(false);
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
|
-
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
70
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
59
71
|
}
|
|
60
72
|
export default SettingsAutoReplyPage;
|
|
61
73
|
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
@@ -61,6 +61,10 @@ function ListSection({ kind, entries, otherEntries, ownAddresses, editable, onAd
|
|
|
61
61
|
setError("That is one of your own addresses, so it cannot be blocked.");
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
+
if (kind === "blocked" && checked.entry.startsWith("@") && ownAddresses.some((address) => address.toLowerCase().endsWith(checked.entry))) {
|
|
65
|
+
setError("That is your own domain: blocking it would send the mail of everyone at it - yours included - to Junk.");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
64
68
|
setError(null);
|
|
65
69
|
setBusy(true);
|
|
66
70
|
try {
|