@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
|
@@ -28,6 +28,8 @@ import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localInde
|
|
|
28
28
|
import { authApiFetch, setApiUnauthorizedObserver } from "../../../../lib/util/api.js";
|
|
29
29
|
import { destroyUnlockedKeys } from "../../../../lib/crypto/keySession.js";
|
|
30
30
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
31
|
+
import { resetNotifications } from "../../notifications/store.js";
|
|
32
|
+
import { clearContactPhotoCache } from "../../../../lib/contacts/useContactPhotoSrc.js";
|
|
31
33
|
import { pluginIcon } from "../../plugins/pluginIcons.js";
|
|
32
34
|
import { mergePluginNavItems } from "../../plugins/pluginNav.js";
|
|
33
35
|
import { useResolvedRailItems } from "../../plugins/useResolvedRailItems.js";
|
|
@@ -181,6 +183,9 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
181
183
|
destroyUnlockedKeys();
|
|
182
184
|
clearPinnedSignerCache();
|
|
183
185
|
clearAppearanceCache();
|
|
186
|
+
clearContactPhotoCache();
|
|
187
|
+
// The notification history (meeting titles, error details) is this session's, and the tab's storage outlives it.
|
|
188
|
+
resetNotifications();
|
|
184
189
|
// Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
|
|
185
190
|
void destroyAllLocalIndexes().then(() => {
|
|
186
191
|
window.location.href = authServerUrl ?? "/";
|
|
@@ -198,6 +203,8 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
198
203
|
// Trusted signer pins read from contacts don't outlive the session either.
|
|
199
204
|
clearPinnedSignerCache();
|
|
200
205
|
clearAppearanceCache();
|
|
206
|
+
clearContactPhotoCache();
|
|
207
|
+
resetNotifications();
|
|
201
208
|
// Open compose windows save edits still waiting on their autosave debounce while the session is still
|
|
202
209
|
// valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
|
|
203
210
|
await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
|
|
@@ -221,6 +228,8 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
|
|
|
221
228
|
// handling, which surfaces via a full reload rather than an inline retry affordance.
|
|
222
229
|
}
|
|
223
230
|
finally {
|
|
231
|
+
// What the impersonated user's session raised (their meetings, their errors) must not be left for the administrator to read.
|
|
232
|
+
resetNotifications();
|
|
224
233
|
window.location.href = "/admin";
|
|
225
234
|
}
|
|
226
235
|
}
|
|
@@ -11,12 +11,11 @@ import FormField from "../../../../lib/components/forms/FormField.js";
|
|
|
11
11
|
import { enrollKey, getKeyVault, VaultAlreadyInitializedError } from "../../../../lib/crypto/keyvaultApi.js";
|
|
12
12
|
import { UnopenableKeysNotice, unlockErrorMessage } from "./UnlockPromptProvider.js";
|
|
13
13
|
import { FrameTakeover } from "../../navigation/frameContext.js";
|
|
14
|
-
import { RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock } from "./RecoveryCodeUnlock.js";
|
|
14
|
+
import { MIN_PASSWORD_LENGTH, RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock, } from "./RecoveryCodeUnlock.js";
|
|
15
15
|
import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, getUnlockedKeys, unlockWithPassword, } from "../../../../lib/crypto/keySession.js";
|
|
16
16
|
import { buildAad, generateMasterKey, sealWithKey } from "../../../../lib/crypto/masterKey.js";
|
|
17
17
|
import { buildPasswordWrap, buildRecoveryWraps } from "../../../../lib/crypto/masterKeyWraps.js";
|
|
18
18
|
import { exportPrivateKeyPkcs8, generateKeyPairWithCsr } from "../../../../lib/crypto/keys.js";
|
|
19
|
-
const MIN_PASSWORD_LENGTH = 8;
|
|
20
19
|
async function provisionEncryptionKey(mailboxUid, mailboxAddress, password) {
|
|
21
20
|
const mk = generateMasterKey();
|
|
22
21
|
const passwordWrap = await buildPasswordWrap(mailboxUid, mk, password);
|
|
@@ -180,7 +179,7 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
180
179
|
}
|
|
181
180
|
if (status === "setup_password" || status === "enrolling") {
|
|
182
181
|
const enrolling = status === "enrolling";
|
|
183
|
-
return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }),
|
|
182
|
+
return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }), _jsxs("p", { className: "text-sm text-text-muted mb-5", children: ["Choose a password to protect your encryption keys. This is separate from your sign-in password and is never sent to the server. Make it ", MIN_PASSWORD_LENGTH, " or more characters: a passphrase of several unrelated words is long enough and easy to remember."] }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSetPassword, children: [_jsx(FormField, { label: "Encryption password", htmlFor: "key-enrollment-password", children: _jsx("input", { id: "key-enrollment-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(FormField, { label: "Confirm password", htmlFor: "key-enrollment-password-confirm", children: _jsx("input", { id: "key-enrollment-password-confirm", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(Button, { type: "submit", loading: enrolling, disabled: enrolling, children: "Continue" })] })] }) }) }));
|
|
184
183
|
}
|
|
185
184
|
if (status === "already_set_up") {
|
|
186
185
|
return (_jsx(FrameTakeover, { children: _jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Encryption is already set up" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "This mailbox\u2019s encryption was set up in another tab or on another device while this page was open, so nothing was changed here. Reload the page and unlock with the password chosen there." }), _jsx(Button, { type: "button", onClick: () => window.location.reload(), children: "Reload" })] }) }) }));
|
|
@@ -13,4 +13,7 @@ import React from "react";
|
|
|
13
13
|
* refused deliberately (a 4xx, e.g. "Automatic mailbox provisioning is not enabled.") so the caller and the
|
|
14
14
|
* administrator they ask can tell why - see `unavailableReason()`.
|
|
15
15
|
*/
|
|
16
|
-
export default function MailboxProvisioning(
|
|
16
|
+
export default function MailboxProvisioning({ impersonating, impersonationBaseUrl }: {
|
|
17
|
+
impersonating?: boolean;
|
|
18
|
+
impersonationBaseUrl?: string;
|
|
19
|
+
}): React.JSX.Element;
|
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
8
|
-
import { autoProvisionMailbox } from "../../../../lib/mail/mailApi.js";
|
|
8
|
+
import { autoProvisionMailbox, stopImpersonating } from "../../../../lib/mail/mailApi.js";
|
|
9
9
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
10
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
11
11
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
@@ -43,7 +43,7 @@ function unavailableReason(err) {
|
|
|
43
43
|
* refused deliberately (a 4xx, e.g. "Automatic mailbox provisioning is not enabled.") so the caller and the
|
|
44
44
|
* administrator they ask can tell why - see `unavailableReason()`.
|
|
45
45
|
*/
|
|
46
|
-
export default function MailboxProvisioning() {
|
|
46
|
+
export default function MailboxProvisioning({ impersonating, impersonationBaseUrl }) {
|
|
47
47
|
const client = useApiClient();
|
|
48
48
|
const [status, setStatus] = useState("checking");
|
|
49
49
|
const [options, setOptions] = useState([]);
|
|
@@ -55,6 +55,12 @@ export default function MailboxProvisioning() {
|
|
|
55
55
|
// Bumped by "Retry" to re-run the check below.
|
|
56
56
|
const [attempt, setAttempt] = useState(0);
|
|
57
57
|
useEffect(() => {
|
|
58
|
+
if (impersonating) {
|
|
59
|
+
// An administrator viewing as a user must never create a mailbox for them just by looking: nothing is provisioned, and the way back is shown
|
|
60
|
+
// (this takeover hides the frame's own "Return to admin" bar).
|
|
61
|
+
setStatus("impersonating");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
58
64
|
setStatus("checking");
|
|
59
65
|
autoProvisionMailbox(undefined, client)
|
|
60
66
|
.then((result) => {
|
|
@@ -87,6 +93,19 @@ export default function MailboxProvisioning() {
|
|
|
87
93
|
}
|
|
88
94
|
});
|
|
89
95
|
}, [attempt]);
|
|
96
|
+
const [returning, setReturning] = useState(false);
|
|
97
|
+
async function handleReturnToAdmin() {
|
|
98
|
+
setReturning(true);
|
|
99
|
+
try {
|
|
100
|
+
await stopImpersonating(impersonationBaseUrl ?? "");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Navigate either way, as the frame's own banner does: a failed call leaves the session as it was.
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
window.location.href = "/admin";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
90
109
|
async function handleConfirm() {
|
|
91
110
|
// `selected` only ever takes a value from `options` itself (the initial default, or the
|
|
92
111
|
// <select>'s own onChange, which can only report one of its own rendered <option> values), so
|
|
@@ -111,6 +130,9 @@ export default function MailboxProvisioning() {
|
|
|
111
130
|
const creating = status === "creating";
|
|
112
131
|
content = (_jsxs("div", { className: "w-full flex flex-col gap-3", children: [_jsx("p", { className: "text-sm text-text-muted text-center", children: "Choose your mailbox address to get started:" }), error && _jsx(Alert, { children: error }), _jsx("select", { "aria-label": "Mailbox address", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: selected, onChange: (e) => setSelected(e.target.value), disabled: creating, children: options.map((option) => (_jsx("option", { value: option.primarySmtpAddress, children: option.primarySmtpAddress }, option.primarySmtpAddress))) }), _jsx(Button, { type: "button", onClick: handleConfirm, loading: creating, disabled: creating, className: "!w-auto self-center", children: "Continue" })] }));
|
|
113
132
|
}
|
|
133
|
+
else if (status === "impersonating") {
|
|
134
|
+
content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "No mailbox" }), _jsx("p", { className: "text-sm text-text-muted", children: "The user you are viewing as has no mailbox, and none is created for them while you are viewing as them." }), _jsx(Button, { type: "button", onClick: () => void handleReturnToAdmin(), loading: returning, disabled: returning, className: "!w-auto self-center", children: "Return to admin" })] }));
|
|
135
|
+
}
|
|
114
136
|
else if (status === "retryable") {
|
|
115
137
|
content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "Couldn\u2019t check right now" }), _jsx("p", { className: "text-sm text-text-muted", children: retryText }), _jsx(Button, { type: "button", onClick: () => setAttempt((n) => n + 1), className: "!w-auto self-center", children: "Retry" })] }));
|
|
116
138
|
}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from "react";
|
|
11
11
|
import { type PublicKey } from "../../../../lib/crypto/keyvaultApi.js";
|
|
12
|
-
/** The same minimum every other "set an encryption password" form in this app uses.
|
|
13
|
-
|
|
12
|
+
/** The same minimum every other "set an encryption password" form in this app uses. The password is the only thing
|
|
13
|
+
* between an offline guesser holding the server's wrapped master key and the mailbox's private keys, so it is
|
|
14
|
+
* long: a passphrase of a few words clears it easily. */
|
|
15
|
+
export declare const MIN_PASSWORD_LENGTH = 12;
|
|
14
16
|
/** Where recovery codes are regenerated. */
|
|
15
17
|
export declare const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
|
|
16
18
|
/** Deliberately generic: a wrong code and a vault with no recovery codes fail the same way. */
|
|
@@ -22,8 +22,10 @@ import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
|
22
22
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
23
23
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
24
24
|
import FormField from "../../../../lib/components/forms/FormField.js";
|
|
25
|
-
/** The same minimum every other "set an encryption password" form in this app uses.
|
|
26
|
-
|
|
25
|
+
/** The same minimum every other "set an encryption password" form in this app uses. The password is the only thing
|
|
26
|
+
* between an offline guesser holding the server's wrapped master key and the mailbox's private keys, so it is
|
|
27
|
+
* long: a passphrase of a few words clears it easily. */
|
|
28
|
+
export const MIN_PASSWORD_LENGTH = 12;
|
|
27
29
|
/** Where recovery codes are regenerated. */
|
|
28
30
|
export const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
|
|
29
31
|
/** Deliberately generic: a wrong code and a vault with no recovery codes fail the same way. */
|
|
@@ -3,13 +3,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from "react-icons/hi2";
|
|
8
8
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
9
|
import { formatMailAddress } from "../../../../lib/mail/mailAddress.js";
|
|
10
10
|
import MailAddress from "./MailAddress.js";
|
|
11
11
|
import { EncryptedPreview, conversationLooksEncrypted, displaySubject } from "./reading/EncryptedPreview.js";
|
|
12
|
-
import { decryptForDisplay, useDecryptedMessages, useUnlockEpoch } from "../../mail/decryptedMessages.js";
|
|
12
|
+
import { decryptForDisplay, mapWithConcurrency, useDecryptedMessages, useUnlockEpoch } from "../../mail/decryptedMessages.js";
|
|
13
13
|
import { listConversationMessages } from "../../../../lib/mail/conversationsApi.js";
|
|
14
14
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
15
15
|
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
|
|
@@ -44,22 +44,64 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
|
|
|
44
44
|
const unlockEpoch = useUnlockEpoch();
|
|
45
45
|
// An encrypted conversation's row stands for its latest message, whose subject only decrypting can tell. Rows are decrypted as they are listed
|
|
46
46
|
// whenever the mailbox is unlocked (and again once it is), the same way the message list does.
|
|
47
|
+
// A few at a time (the list can hold hundreds), and none more once the list has been replaced by a newer one.
|
|
47
48
|
useEffect(() => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
let stale = false;
|
|
50
|
+
void mapWithConcurrency(conversations.filter(conversationLooksEncrypted), 4, async (conversation) => {
|
|
51
|
+
if (!stale) {
|
|
52
|
+
await decryptForDisplay(conversation.latestMessageUid, conversation.mailboxUid ?? mailboxUid);
|
|
51
53
|
}
|
|
52
|
-
}
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
stale = true;
|
|
57
|
+
};
|
|
53
58
|
}, [conversations, mailboxUid, unlockEpoch]);
|
|
54
59
|
const [expanded, setExpanded] = useState(new Set());
|
|
55
60
|
const [messagesById, setMessagesById] = useState({});
|
|
56
61
|
const [loadingIds, setLoadingIds] = useState(new Set());
|
|
57
62
|
const [errorsById, setErrorsById] = useState({});
|
|
63
|
+
// The messages each cached thread was fetched for (its `messageUids`): a list that now says otherwise has the thread fetched again.
|
|
64
|
+
const fetchedForRef = useRef({});
|
|
58
65
|
// The answers given from a row's RSVP chip, by message uid: a row draws them at once, whichever row (the conversation's, or one of its messages) they were given from.
|
|
59
66
|
const [answers, setAnswers] = useState({});
|
|
60
67
|
function answered(updated) {
|
|
61
68
|
setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
|
|
62
69
|
}
|
|
70
|
+
// New mail, a delete or a move changes a conversation's messages: what was fetched of it is out of date. An expanded thread is fetched again
|
|
71
|
+
// in place (the rows it shows stay until the new ones arrive); a collapsed one is forgotten, to be fetched when next opened. A conversation
|
|
72
|
+
// that left the list is forgotten too.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const listed = new Set();
|
|
75
|
+
const stale = [];
|
|
76
|
+
for (const conversation of conversations) {
|
|
77
|
+
const id = conversationRowKey(conversation);
|
|
78
|
+
listed.add(id);
|
|
79
|
+
const fetchedFor = fetchedForRef.current[id];
|
|
80
|
+
if (fetchedFor === undefined || fetchedFor === conversation.messageUids.join(",")) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (expanded.has(id)) {
|
|
84
|
+
fetchedForRef.current[id] = conversation.messageUids.join(",");
|
|
85
|
+
listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
|
|
86
|
+
.then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
|
|
87
|
+
.catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
stale.push(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const id of Object.keys(fetchedForRef.current)) {
|
|
94
|
+
if (!listed.has(id)) {
|
|
95
|
+
stale.push(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (stale.length > 0) {
|
|
99
|
+
for (const id of stale) {
|
|
100
|
+
delete fetchedForRef.current[id];
|
|
101
|
+
}
|
|
102
|
+
setMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => !stale.includes(id))));
|
|
103
|
+
}
|
|
104
|
+
}, [conversations]);
|
|
63
105
|
function toggle(conversation) {
|
|
64
106
|
const id = conversationRowKey(conversation);
|
|
65
107
|
setExpanded((prev) => {
|
|
@@ -81,6 +123,7 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
|
|
|
81
123
|
delete next[id];
|
|
82
124
|
return next;
|
|
83
125
|
});
|
|
126
|
+
fetchedForRef.current[id] = conversation.messageUids.join(",");
|
|
84
127
|
listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
|
|
85
128
|
.then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
|
|
86
129
|
.catch((err) => setErrorsById((prev) => ({
|
|
@@ -1,78 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* draws the checkmark column; without it the row is a plain command. */
|
|
4
|
-
interface MenuItemBase {
|
|
5
|
-
key: string;
|
|
6
|
-
label: string;
|
|
7
|
-
/** A one-line explanation under the label, for an item whose effect isn't obvious from its name. */
|
|
8
|
-
description?: string;
|
|
9
|
-
role?: "menuitem" | "menuitemradio" | "menuitemcheckbox";
|
|
10
|
-
/** `"mixed"` is ARIA's third checkbox state - some of the things this row acts on have it and some
|
|
11
|
-
* don't, which is what a label applied to only part of a multi-message selection looks like. */
|
|
12
|
-
checked?: boolean | "mixed";
|
|
13
|
-
/** A colour swatch before the label - a label's own colour. */
|
|
14
|
-
swatchColor?: string;
|
|
15
|
-
/** An icon before the label, for a menu whose rows are commands (the reading pane's "More actions"). */
|
|
16
|
-
icon?: ReactNode;
|
|
17
|
-
/** A tooltip for the row - the full text of a label the row truncates. */
|
|
18
|
-
title?: string;
|
|
19
|
-
disabled?: boolean;
|
|
20
|
-
/** Leaves the menu open after choosing this row, for a multi-select list where several rows are ticked
|
|
21
|
-
* before one command commits them all. */
|
|
22
|
-
keepOpen?: boolean;
|
|
23
|
-
}
|
|
24
|
-
/** A row that does something when chosen. */
|
|
25
|
-
export interface MenuCommandSpec extends MenuItemBase {
|
|
26
|
-
onSelect: () => void;
|
|
27
|
-
submenu?: never;
|
|
28
|
-
}
|
|
29
|
-
/** A row that opens a submenu instead: choosing it replaces the menu's contents with these sections, under
|
|
30
|
-
* a Back row. One level deep, which is all any menu here needs - so it has nothing of its own to do. */
|
|
31
|
-
export interface MenuSubmenuSpec extends MenuItemBase {
|
|
32
|
-
submenu: MenuSectionSpec[];
|
|
33
|
-
onSelect?: never;
|
|
34
|
-
}
|
|
35
|
-
export type MenuItemSpec = MenuCommandSpec | MenuSubmenuSpec;
|
|
36
|
-
/** A labelled group of items. Groups after the first are drawn with a separator above them. */
|
|
37
|
-
export interface MenuSectionSpec {
|
|
38
|
-
key: string;
|
|
39
|
-
label?: string;
|
|
40
|
-
/** A short note under the group's items - e.g. why some of them are unavailable right now. */
|
|
41
|
-
note?: string;
|
|
42
|
-
items: MenuItemSpec[];
|
|
43
|
-
}
|
|
44
|
-
/** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
|
|
45
|
-
export declare function menuHeight(sections: MenuSectionSpec[], width?: number): number;
|
|
46
|
-
export interface MenuButtonProps {
|
|
47
|
-
/** The trigger's visible label. Not drawn (nor is the chevron) with `iconOnly`, where `aria-label` is all the trigger says. */
|
|
48
|
-
label: ReactNode;
|
|
49
|
-
/** The trigger is just its `icon` - a round icon button like the reading pane's own command row, whose `className` it takes over
|
|
50
|
-
* completely. `aria-label` (and `title`) name it. */
|
|
51
|
-
iconOnly?: boolean;
|
|
52
|
-
/** The trigger's accessible name, which also names the menu itself - includes the current selection
|
|
53
|
-
* where there is one ("Filter: Unread"), so the button says what it's set to, not just what it does. */
|
|
54
|
-
"aria-label": string;
|
|
55
|
-
icon?: ReactNode;
|
|
56
|
-
disabled?: boolean;
|
|
57
|
-
title?: string;
|
|
58
|
-
sections: MenuSectionSpec[];
|
|
59
|
-
width?: number;
|
|
60
|
-
/** Extra classes for the trigger, on top of the shared toolbar-button styling. */
|
|
61
|
-
className?: string;
|
|
62
|
-
/** Told whenever the menu opens or closes - how a caller resets a draft it keeps for the menu's own
|
|
63
|
-
* multi-select rows (see `useLabelDraft()`). */
|
|
64
|
-
onOpenChange?: (open: boolean) => void;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* A toolbar button that opens an ARIA menu: `role="menu"` rows with checkmarks for the current choice,
|
|
68
|
-
* full keyboard support (Enter/Space or Arrow Down to open, arrows/Home/End to move with wrap, Enter/Space
|
|
69
|
-
* to choose, Escape or Tab to close with focus returning to the trigger) and a click outside to dismiss.
|
|
70
|
-
*
|
|
71
|
-
* Rendered through `PopoverPortal` rather than as an absolutely-positioned child: the mail list toolbar
|
|
72
|
-
* lives inside the list's own `overflow-y-auto` scroll container, which clips an absolute popup however
|
|
73
|
-
* high its `z-index` - the same reason Compose's own pickers portal out (see `PopoverPortal`'s own doc
|
|
74
|
-
* comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
|
|
75
|
-
* semantics, roving focus and keyboard handling are this component's own.
|
|
76
|
-
*/
|
|
77
|
-
export default function MenuButton({ label, iconOnly, icon, disabled, title, sections, width, className, "aria-label": ariaLabel, onOpenChange, }: MenuButtonProps): React.JSX.Element;
|
|
78
|
-
export {};
|
|
1
|
+
export { default, menuHeight } from "../../../../lib/components/menus/MenuButton.js";
|
|
2
|
+
export type { MenuCommandSpec, MenuSubmenuSpec, MenuItemSpec, MenuSectionSpec, MenuButtonProps } from "../../../../lib/components/menus/MenuButton.js";
|
|
@@ -1,215 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
1
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import PopoverPortal from "../../../../lib/components/overlays/PopoverPortal.js";
|
|
9
|
-
// `PopoverPortal` positions a fixed-size box (it has no auto-height mode), so the menu's height is
|
|
10
|
-
// computed from its own contents rather than measured - from whichever level is *shown*, so a submenu
|
|
11
|
-
// isn't left standing in the parent menu's taller box. These are the exact heights the classes below
|
|
12
|
-
// render at, so the box is never short enough to clip its last row: an item is `h-9`, a group label
|
|
13
|
-
// `h-6`, a note as many `leading-4` lines as it wraps to, a separator a 1px rule inside `my-1`, and the
|
|
14
|
-
// list itself `py-1`.
|
|
15
|
-
const ITEM_HEIGHT = 36;
|
|
16
|
-
const ITEM_DESCRIPTION_HEIGHT = 16;
|
|
17
|
-
const GROUP_LABEL_HEIGHT = 24;
|
|
18
|
-
const NOTE_LINE_HEIGHT = 16;
|
|
19
|
-
const NOTE_PADDING = 4;
|
|
20
|
-
/** A note wraps, so its height depends on how much of it fits a line: `text-xs` averages a little over 6px
|
|
21
|
-
* a character, and the note sits inside the list's `px-3`. Rounded so the estimate is never *under* the
|
|
22
|
-
* lines the browser actually draws - a box a few pixels too tall shows blank space, one too short clips. */
|
|
23
|
-
const NOTE_CHAR_WIDTH = 6.4;
|
|
24
|
-
const NOTE_PADDING_X = 24;
|
|
25
|
-
const SEPARATOR_HEIGHT = 9;
|
|
26
|
-
const LIST_PADDING = 8;
|
|
27
|
-
const MENU_MAX_HEIGHT = 460;
|
|
28
|
-
const DEFAULT_MENU_WIDTH = 248;
|
|
29
|
-
/** How tall `note` renders at `width`, wrapped. */
|
|
30
|
-
function noteHeight(note, width) {
|
|
31
|
-
const charsPerLine = Math.max(1, Math.floor((width - NOTE_PADDING_X) / NOTE_CHAR_WIDTH));
|
|
32
|
-
return Math.ceil(note.length / charsPerLine) * NOTE_LINE_HEIGHT + NOTE_PADDING;
|
|
33
|
-
}
|
|
34
|
-
/** The height `PopoverPortal` is asked for - exact for a short menu, capped (the list scrolls) for a long one. */
|
|
35
|
-
export function menuHeight(sections, width = DEFAULT_MENU_WIDTH) {
|
|
36
|
-
let height = LIST_PADDING;
|
|
37
|
-
sections.forEach((section, index) => {
|
|
38
|
-
if (index > 0) {
|
|
39
|
-
height += SEPARATOR_HEIGHT;
|
|
40
|
-
}
|
|
41
|
-
if (section.label) {
|
|
42
|
-
height += GROUP_LABEL_HEIGHT;
|
|
43
|
-
}
|
|
44
|
-
for (const item of section.items) {
|
|
45
|
-
height += ITEM_HEIGHT + (item.description ? ITEM_DESCRIPTION_HEIGHT : 0);
|
|
46
|
-
}
|
|
47
|
-
if (section.note) {
|
|
48
|
-
height += noteHeight(section.note, width);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
return Math.min(height, MENU_MAX_HEIGHT);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* A toolbar button that opens an ARIA menu: `role="menu"` rows with checkmarks for the current choice,
|
|
55
|
-
* full keyboard support (Enter/Space or Arrow Down to open, arrows/Home/End to move with wrap, Enter/Space
|
|
56
|
-
* to choose, Escape or Tab to close with focus returning to the trigger) and a click outside to dismiss.
|
|
57
|
-
*
|
|
58
|
-
* Rendered through `PopoverPortal` rather than as an absolutely-positioned child: the mail list toolbar
|
|
59
|
-
* lives inside the list's own `overflow-y-auto` scroll container, which clips an absolute popup however
|
|
60
|
-
* high its `z-index` - the same reason Compose's own pickers portal out (see `PopoverPortal`'s own doc
|
|
61
|
-
* comment). The portal supplies the positioning, the outside-click dismissal and the box; the menu
|
|
62
|
-
* semantics, roving focus and keyboard handling are this component's own.
|
|
63
|
-
*/
|
|
64
|
-
export default function MenuButton({ label, iconOnly = false, icon, disabled, title, sections, width = DEFAULT_MENU_WIDTH, className = "", "aria-label": ariaLabel, onOpenChange, }) {
|
|
65
|
-
const [open, setOpen] = useState(false);
|
|
66
|
-
const [activeIndex, setActiveIndex] = useState(0);
|
|
67
|
-
// The key of the item whose submenu is showing, or `null` at the top level. Held as a key, not as the
|
|
68
|
-
// item itself, because `sections` is rebuilt on every render.
|
|
69
|
-
const [submenuKey, setSubmenuKey] = useState(null);
|
|
70
|
-
const triggerRef = useRef(null);
|
|
71
|
-
const itemRefs = useRef([]);
|
|
72
|
-
// State, not a ref: `PopoverPortal` renders nothing at all until its own positioning effect has run, so
|
|
73
|
-
// the rows don't exist yet when this component's effects first fire. Keying the focus effect on the
|
|
74
|
-
// menu node itself makes it run again once they do.
|
|
75
|
-
const [menuNode, setMenuNode] = useState(null);
|
|
76
|
-
const openSubmenu = sections
|
|
77
|
-
.flatMap((section) => section.items)
|
|
78
|
-
.find((item) => item.key === submenuKey && !!item.submenu);
|
|
79
|
-
/** The row that leaves a submenu again - synthesized rather than asked of the caller, so every submenu
|
|
80
|
-
* has the same way back however it was built. */
|
|
81
|
-
const backItem = {
|
|
82
|
-
key: "__back",
|
|
83
|
-
label: `Back to ${ariaLabel}`,
|
|
84
|
-
keepOpen: true,
|
|
85
|
-
onSelect: () => leaveSubmenu(),
|
|
86
|
-
};
|
|
87
|
-
const shownSections = openSubmenu
|
|
88
|
-
? [{ key: "__back", items: [backItem] }, ...openSubmenu.submenu]
|
|
89
|
-
: sections;
|
|
90
|
-
const items = shownSections.flatMap((section) => section.items);
|
|
91
|
-
const enabledIndexes = items.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index !== -1);
|
|
92
|
-
/** The row focus starts on: the current choice where there is one, else the first row that can take focus. */
|
|
93
|
-
function initialActiveIndex(within) {
|
|
94
|
-
const checked = within.findIndex((item) => item.checked === true && !item.disabled);
|
|
95
|
-
const enabled = within.map((item, index) => (item.disabled ? -1 : index)).filter((index) => index !== -1);
|
|
96
|
-
return checked === -1 ? (enabled[0] ?? 0) : checked;
|
|
97
|
-
}
|
|
98
|
-
function openMenu() {
|
|
99
|
-
setSubmenuKey(null);
|
|
100
|
-
setActiveIndex(initialActiveIndex(sections.flatMap((section) => section.items)));
|
|
101
|
-
setOpen(true);
|
|
102
|
-
onOpenChange?.(true);
|
|
103
|
-
}
|
|
104
|
-
function closeMenu(returnFocus) {
|
|
105
|
-
setOpen(false);
|
|
106
|
-
setSubmenuKey(null);
|
|
107
|
-
onOpenChange?.(false);
|
|
108
|
-
if (returnFocus) {
|
|
109
|
-
triggerRef.current?.focus();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function enterSubmenu(item) {
|
|
113
|
-
setSubmenuKey(item.key);
|
|
114
|
-
const rows = item.submenu.flatMap((section) => section.items);
|
|
115
|
-
// Past the Back row, onto the first row of the submenu itself - or, when none of them can take focus (every row is unavailable right now), onto
|
|
116
|
-
// the Back row, so the keyboard still has a place to be.
|
|
117
|
-
setActiveIndex(rows.some((row) => !row.disabled) ? 1 + initialActiveIndex(rows) : 0);
|
|
118
|
-
}
|
|
119
|
-
function leaveSubmenu() {
|
|
120
|
-
// Back on the row the submenu was opened from.
|
|
121
|
-
const parentIndex = sections.flatMap((section) => section.items).findIndex((item) => item.key === submenuKey);
|
|
122
|
-
setSubmenuKey(null);
|
|
123
|
-
setActiveIndex(parentIndex);
|
|
124
|
-
}
|
|
125
|
-
/** What a row does when it is chosen: open its submenu, or run it and close unless it asked to stay. */
|
|
126
|
-
function selectItem(item) {
|
|
127
|
-
if (item.submenu) {
|
|
128
|
-
enterSubmenu(item);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
item.onSelect();
|
|
132
|
-
if (!item.keepOpen) {
|
|
133
|
-
closeMenu(true);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Roving focus: only the active row is tabbable, and it takes DOM focus whenever it changes, so the
|
|
137
|
-
// arrow keys move the screen reader's cursor too rather than only a visual highlight.
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (open && menuNode) {
|
|
140
|
-
// A disabled row can't take focus, so a menu with nothing enabled focuses its own container -
|
|
141
|
-
// otherwise the keys it handles would go to whatever had focus before it opened.
|
|
142
|
-
if (enabledIndexes.length === 0) {
|
|
143
|
-
menuNode.focus();
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
itemRefs.current[activeIndex]?.focus();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// `submenuKey` too: drilling in or out can land on the same index in the other level's list, and
|
|
150
|
-
// the row that index *means* is a different button, which the effect must move focus to.
|
|
151
|
-
}, [open, activeIndex, submenuKey, menuNode]);
|
|
152
|
-
function moveActive(delta) {
|
|
153
|
-
if (enabledIndexes.length === 0) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const position = enabledIndexes.indexOf(activeIndex);
|
|
157
|
-
setActiveIndex(enabledIndexes[(position + delta + enabledIndexes.length) % enabledIndexes.length]);
|
|
158
|
-
}
|
|
159
|
-
function handleMenuKeyDown(e) {
|
|
160
|
-
if (e.key === "ArrowDown") {
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
moveActive(1);
|
|
163
|
-
}
|
|
164
|
-
else if (e.key === "ArrowUp") {
|
|
165
|
-
e.preventDefault();
|
|
166
|
-
moveActive(-1);
|
|
167
|
-
}
|
|
168
|
-
else if (e.key === "Home") {
|
|
169
|
-
e.preventDefault();
|
|
170
|
-
setActiveIndex(enabledIndexes[0] ?? 0);
|
|
171
|
-
}
|
|
172
|
-
else if (e.key === "End") {
|
|
173
|
-
e.preventDefault();
|
|
174
|
-
setActiveIndex(enabledIndexes[enabledIndexes.length - 1] ?? 0);
|
|
175
|
-
}
|
|
176
|
-
else if (e.key === "ArrowRight" && items[activeIndex]?.submenu) {
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
enterSubmenu(items[activeIndex]);
|
|
179
|
-
}
|
|
180
|
-
else if ((e.key === "ArrowLeft" || e.key === "Escape") && openSubmenu) {
|
|
181
|
-
// ARIA's own submenu behaviour: Escape leaves the submenu for its parent menu rather than
|
|
182
|
-
// dismissing the whole thing.
|
|
183
|
-
e.preventDefault();
|
|
184
|
-
leaveSubmenu();
|
|
185
|
-
}
|
|
186
|
-
else if (e.key === "Escape" || e.key === "Tab") {
|
|
187
|
-
e.preventDefault();
|
|
188
|
-
closeMenu(true);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function handleTriggerKeyDown(e) {
|
|
192
|
-
if (!open && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
193
|
-
e.preventDefault();
|
|
194
|
-
openMenu();
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
let itemIndex = -1;
|
|
198
|
-
return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": ariaLabel, title: title, disabled: disabled, onClick: () => (open ? closeMenu(false) : openMenu()), onKeyDown: handleTriggerKeyDown, className: iconOnly
|
|
199
|
-
? className
|
|
200
|
-
: [
|
|
201
|
-
// `min-w-0` so a long label ("Filter: Has attachments") truncates instead of pushing
|
|
202
|
-
// whatever sits beside it in the toolbar off the edge of a 384px list column.
|
|
203
|
-
"inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent",
|
|
204
|
-
className,
|
|
205
|
-
].join(" "), children: [icon, !iconOnly && _jsx("span", { className: "truncate", children: label }), !iconOnly && _jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: "shrink-0 text-text-muted" })] }), open && (_jsx(PopoverPortal, { anchorRef: triggerRef, onClose: () => closeMenu(false), width: width, height: menuHeight(shownSections, width), "aria-label": ariaLabel, children: _jsx("div", { ref: setMenuNode, role: "menu", tabIndex: -1, "aria-label": ariaLabel, onKeyDown: handleMenuKeyDown, className: "flex-1 overflow-y-auto py-1", children: shownSections.map((section, sectionIndex) => (_jsxs("div", { role: "group", "aria-label": section.label, className: sectionIndex > 0 ? "border-t border-border mt-1 pt-1" : "", children: [section.label && (_jsx("div", { "aria-hidden": "true", className: "h-6 flex items-center px-3 text-xs font-semibold uppercase tracking-wide text-text-muted", children: section.label })), section.items.map((item) => {
|
|
206
|
-
itemIndex += 1;
|
|
207
|
-
const index = itemIndex;
|
|
208
|
-
const role = item.role ?? "menuitem";
|
|
209
|
-
return (_jsxs("button", { ref: (node) => {
|
|
210
|
-
itemRefs.current[index] = node;
|
|
211
|
-
}, type: "button", role: role, disabled: item.disabled, title: item.title, tabIndex: index === activeIndex ? 0 : -1, ...(role === "menuitem"
|
|
212
|
-
? {}
|
|
213
|
-
: { "aria-checked": item.checked === "mixed" ? "mixed" : !!item.checked }), ...(item.submenu ? { "aria-haspopup": "menu", "aria-expanded": false } : {}), onClick: () => selectItem(item), className: "w-full flex items-start gap-2 px-3 py-2 text-left text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsxs("span", { className: "w-4 shrink-0 flex justify-center pt-0.5", children: [item.key === "__back" && (_jsx(HiChevronLeft, { size: 14, "aria-hidden": "true", className: "text-text-muted" })), item.checked === "mixed" ? (_jsx(HiMinus, { size: 14, "aria-hidden": "true", className: "text-primary-dark" })) : (item.checked && _jsx(HiCheck, { size: 14, "aria-hidden": "true", className: "text-primary-dark" }))] }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex h-5 items-center gap-1.5", children: [item.icon && (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted", children: item.icon })), item.swatchColor && (_jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: item.swatchColor } })), _jsx("span", { className: "truncate", children: item.label })] }), item.description && (_jsx("span", { className: "block h-4 text-xs leading-4 text-text-muted truncate font-normal", children: item.description }))] }), item.submenu && (_jsx(HiChevronRight, { size: 14, "aria-hidden": "true", className: "shrink-0 mt-0.5 text-text-muted" }))] }, item.key));
|
|
214
|
-
}), section.note && _jsx("p", { className: "px-3 py-1 text-xs leading-4 text-text-muted", children: section.note })] }, section.key))) }) }))] }));
|
|
215
|
-
}
|
|
5
|
+
// `MenuButton` lives in `lib/components/menus` so shared form components (`ImageEditBadge`) can use it; this keeps the mail components' import path working.
|
|
6
|
+
export { default, menuHeight } from "../../../../lib/components/menus/MenuButton.js";
|
|
@@ -48,7 +48,7 @@ import LabelMenuButton from "./labelMenu.js";
|
|
|
48
48
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
49
49
|
import EncryptedBody from "./reading/EncryptedBody.js";
|
|
50
50
|
import { ENCRYPTED_SUBJECT_PLACEHOLDER, displaySubject } from "./reading/EncryptedPreview.js";
|
|
51
|
-
import { rememberDecrypted } from "../../mail/decryptedMessages.js";
|
|
51
|
+
import { mapWithConcurrency, rememberDecrypted } from "../../mail/decryptedMessages.js";
|
|
52
52
|
import MessageBody, { BodySkeleton } from "./reading/MessageBody.js";
|
|
53
53
|
import { BODY_FONT_STYLE, CardShell, SenderAvatar, SubjectCard } from "./reading/MessageCard.js";
|
|
54
54
|
import MessageMoreMenu from "./reading/MessageMoreMenu.js";
|
|
@@ -210,6 +210,8 @@ function subjectCore(subject) {
|
|
|
210
210
|
function sameSubject(subject, threadSubject) {
|
|
211
211
|
return subjectCore(subject) === subjectCore(threadSubject);
|
|
212
212
|
}
|
|
213
|
+
/** How many earlier messages of a conversation are loaded at once when printing it. */
|
|
214
|
+
const PRINT_CONCURRENCY = 4;
|
|
213
215
|
function formatBytes(bytes) {
|
|
214
216
|
if (bytes >= 1000000)
|
|
215
217
|
return `${(bytes / 1000000).toFixed(1)} MB`;
|
|
@@ -779,10 +781,11 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
779
781
|
(verifiedOrSealed || security.state === "encrypted" || security.state === "encrypted_unverified_signer" || security.state === "signature_failed")
|
|
780
782
|
? security.attachments
|
|
781
783
|
: undefined;
|
|
782
|
-
// Any state
|
|
784
|
+
// Any state with a valid signature shows the address actually signed for (the protected From when the message
|
|
783
785
|
// carries one - it equals the outer From's address, or verification would have failed) next to the badge, never a
|
|
784
|
-
// display name alone: anyone can put "ceo@corp.com" in the name of a message sent from x@corp-pay.com.
|
|
785
|
-
|
|
786
|
+
// display name alone: anyone can put "ceo@corp.com" in the name of a message sent from x@corp-pay.com. A failed
|
|
787
|
+
// signature vouches for nothing, so its message shows the outer address like an unsigned one.
|
|
788
|
+
const signatureShown = security !== null && (verifiedOrSealed || UNVERIFIED_SIGNER_STATES.has(security.state));
|
|
786
789
|
const senderAddress = (signatureShown ? extractAddresses(security.protectedHeaders?.from)[0] : undefined) ?? message.from.address;
|
|
787
790
|
const senderName = message.from.displayName;
|
|
788
791
|
const senderNameCheck = checkSenderName(senderName, senderAddress);
|
|
@@ -841,11 +844,12 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
841
844
|
try {
|
|
842
845
|
const content = bodyContent ?? (await fetchBodyContent(message.uid, message.version));
|
|
843
846
|
const older = await olderConversationMessages(message, client).catch(() => []);
|
|
844
|
-
|
|
847
|
+
// A few at a time: a long conversation is hundreds of messages, each of them several requests.
|
|
848
|
+
const earlier = await mapWithConcurrency(older, PRINT_CONCURRENCY, (other) => printableOlderMessage(other).catch(() => ({
|
|
845
849
|
subject: other.subject || "(no subject)",
|
|
846
850
|
headers: printHeaders(other, formatMailAddress(other.from)),
|
|
847
851
|
content: { kind: "text", text: "This message could not be loaded, so it is not printed." },
|
|
848
|
-
})))
|
|
852
|
+
})));
|
|
849
853
|
const printable = buildConversationPrintDocument(shownSubject, [
|
|
850
854
|
{
|
|
851
855
|
subject: shownSubject,
|