@rapidmx/web-client 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +21 -2
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { useEffect,
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
6
|
import ContactAvatar from "../../../../lib/components/avatar/ContactAvatar.js";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import { ContactPhotoError, prepareContactPhoto } from "../../../../lib/contacts/preparePhoto.js";
|
|
8
|
+
import ImageEditBadge from "../../../../lib/components/forms/ImageEditBadge.js";
|
|
9
9
|
|
|
10
10
|
export interface ContactPhotoFieldProps {
|
|
11
11
|
displayName: string;
|
|
12
12
|
/** The contact's first address, for the Gravatar shown while they have no picture of their own. */
|
|
13
13
|
email?: string;
|
|
14
|
-
/** The picture the contact has now (`
|
|
14
|
+
/** The picture the contact has now (`useContactPhotoSrc()`), if any. */
|
|
15
15
|
currentUrl?: string;
|
|
16
16
|
/** The picture chosen to replace it, not uploaded until the contact is saved; `null` for none. */
|
|
17
17
|
picked: File | null;
|
|
@@ -21,15 +21,18 @@ export interface ContactPhotoFieldProps {
|
|
|
21
21
|
onRemove: () => void;
|
|
22
22
|
/** Called with the reason a chosen file was turned down. */
|
|
23
23
|
onReject: (message: string) => void;
|
|
24
|
+
/** Called with `true` while a chosen file is being made ready, so the form is not saved before it is, and with `false` after. */
|
|
25
|
+
onBusyChange: (busy: boolean) => void;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
* The picture of a contact in the edit form: the avatar as it is (their picture, else Gravatar, else initials) with
|
|
28
|
-
* there is a picture of their own, "Remove photo".
|
|
29
|
+
* The picture of a contact in the edit form: the avatar as it is (their picture, else Gravatar, else initials) with a camera badge at its lower right that opens a
|
|
30
|
+
* menu (`ImageEditBadge`) - "Upload file", "Take photo" and, when there is a picture of their own, "Remove photo". Any picture is taken (a phone's camera photo of several megabytes,
|
|
31
|
+
* in any orientation, included): it is made small enough here by `prepareContactPhoto()` and only uploaded when the form is saved.
|
|
29
32
|
*/
|
|
30
|
-
export default function ContactPhotoField({ displayName, email, currentUrl, picked, removed, onPick, onRemove, onReject }: ContactPhotoFieldProps) {
|
|
31
|
-
const input = useRef<HTMLInputElement>(null);
|
|
33
|
+
export default function ContactPhotoField({ displayName, email, currentUrl, picked, removed, onPick, onRemove, onReject, onBusyChange }: ContactPhotoFieldProps) {
|
|
32
34
|
const [preview, setPreview] = useState<string | undefined>();
|
|
35
|
+
const [preparing, setPreparing] = useState(false);
|
|
33
36
|
|
|
34
37
|
useEffect(() => {
|
|
35
38
|
if (!picked) {
|
|
@@ -41,16 +44,16 @@ export default function ContactPhotoField({ displayName, email, currentUrl, pick
|
|
|
41
44
|
return () => URL.revokeObjectURL(url);
|
|
42
45
|
}, [picked]);
|
|
43
46
|
|
|
44
|
-
function handleChange(file: File
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
async function handleChange(file: File) {
|
|
48
|
+
setPreparing(true);
|
|
49
|
+
onBusyChange(true);
|
|
50
|
+
try {
|
|
51
|
+
onPick(await prepareContactPhoto(file));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
onReject(err instanceof ContactPhotoError ? err.message : "This picture could not be read - choose another one.");
|
|
54
|
+
} finally {
|
|
55
|
+
setPreparing(false);
|
|
56
|
+
onBusyChange(false);
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -58,29 +61,23 @@ export default function ContactPhotoField({ displayName, email, currentUrl, pick
|
|
|
58
61
|
const hasOwn = !!picked || (!!currentUrl && !removed);
|
|
59
62
|
return (
|
|
60
63
|
<div className="flex items-center gap-4 mb-3">
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
handleChange(e.target.files?.[0]);
|
|
72
|
-
e.target.value = "";
|
|
73
|
-
}}
|
|
64
|
+
<div className="relative inline-block shrink-0">
|
|
65
|
+
<ContactAvatar displayName={displayName || "?"} size={72} photoUrl={shown} email={picked || (currentUrl && !removed) ? undefined : email} />
|
|
66
|
+
<ImageEditBadge
|
|
67
|
+
position="bottom-right"
|
|
68
|
+
label="Change contact photo"
|
|
69
|
+
fileInputLabel="Contact photo file"
|
|
70
|
+
hasImage={hasOwn}
|
|
71
|
+
busy={preparing}
|
|
72
|
+
onFile={(file) => void handleChange(file)}
|
|
73
|
+
onRemove={onRemove}
|
|
74
74
|
/>
|
|
75
|
-
<Button type="button" variant="secondary" className="!w-auto !py-1 !px-3 text-xs" onClick={() => input.current?.click()}>
|
|
76
|
-
{hasOwn ? "Change photo" : "Add photo"}
|
|
77
|
-
</Button>
|
|
78
|
-
{hasOwn && (
|
|
79
|
-
<Button type="button" variant="secondary" className="!w-auto !py-1 !px-3 text-xs" onClick={onRemove}>
|
|
80
|
-
Remove photo
|
|
81
|
-
</Button>
|
|
82
|
-
)}
|
|
83
75
|
</div>
|
|
76
|
+
{preparing && (
|
|
77
|
+
<span role="status" className="text-xs text-text-muted">
|
|
78
|
+
Preparing picture…
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
84
81
|
</div>
|
|
85
82
|
);
|
|
86
83
|
}
|
|
@@ -113,7 +113,7 @@ export default function ContactsShell({
|
|
|
113
113
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
114
114
|
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
115
115
|
if (userUid && status === "ready" && !mailboxUid) {
|
|
116
|
-
return <MailboxProvisioning />;
|
|
116
|
+
return <MailboxProvisioning impersonating={impersonating} impersonationBaseUrl={impersonationBaseUrl} />;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
let inner: ReactNode = null;
|
|
@@ -128,15 +128,32 @@ export function contactInputFromCard(card: ParsedVCardContact, scope: { mailboxU
|
|
|
128
128
|
return { ...card, ...scope, ...names, emails: card.emails ?? [], phones: card.phones ?? [], addresses: card.addresses ?? [] };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
131
|
+
/** What a card with no email is told apart by: its name and its first phone number (digits only). */
|
|
132
|
+
function identityKey(person: { displayName: string; phones?: readonly { phoneNumber: string }[] }): string {
|
|
133
|
+
return `${person.displayName.trim().toLowerCase()}|${(person.phones?.[0]?.phoneNumber ?? "").replace(/\D/g, "")}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The cards that are not yet in the address book, and how many were: a card is a duplicate when one of its emails is a stored contact's or an earlier card's,
|
|
138
|
+
* and a card with no email at all when its name and first phone number are a stored contact's or an earlier card's (so importing the same file twice adds nothing).
|
|
139
|
+
*/
|
|
132
140
|
export function withoutDuplicates(
|
|
133
141
|
cards: readonly ParsedVCardContact[],
|
|
134
142
|
existing: readonly Contact[],
|
|
135
143
|
): { fresh: ParsedVCardContact[]; skipped: number } {
|
|
136
144
|
const known = new Set(existing.flatMap((contact) => contact.emails.map((email) => normalizeAddress(email.address))));
|
|
145
|
+
const knownIdentities = new Set(existing.map(identityKey));
|
|
137
146
|
const fresh: ParsedVCardContact[] = [];
|
|
138
147
|
for (const card of cards) {
|
|
139
148
|
const addresses = (card.emails ?? []).map((email) => normalizeAddress(email.address));
|
|
149
|
+
if (addresses.length === 0) {
|
|
150
|
+
const identity = identityKey(card);
|
|
151
|
+
if (!knownIdentities.has(identity)) {
|
|
152
|
+
knownIdentities.add(identity);
|
|
153
|
+
fresh.push(card);
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
140
157
|
if (addresses.some((address) => known.has(address))) {
|
|
141
158
|
continue;
|
|
142
159
|
}
|
|
@@ -34,6 +34,8 @@ import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localInde
|
|
|
34
34
|
import { authApiFetch, setApiUnauthorizedObserver } from "../../../../lib/util/api.js";
|
|
35
35
|
import { destroyUnlockedKeys } from "../../../../lib/crypto/keySession.js";
|
|
36
36
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
37
|
+
import { resetNotifications } from "../../notifications/store.js";
|
|
38
|
+
import { clearContactPhotoCache } from "../../../../lib/contacts/useContactPhotoSrc.js";
|
|
37
39
|
import { pluginIcon } from "../../plugins/pluginIcons.js";
|
|
38
40
|
import { mergePluginNavItems, PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
|
|
39
41
|
import { type ResolvedRailLinks, useResolvedRailItems } from "../../plugins/useResolvedRailItems.js";
|
|
@@ -266,6 +268,9 @@ export function AppChrome({
|
|
|
266
268
|
destroyUnlockedKeys();
|
|
267
269
|
clearPinnedSignerCache();
|
|
268
270
|
clearAppearanceCache();
|
|
271
|
+
clearContactPhotoCache();
|
|
272
|
+
// The notification history (meeting titles, error details) is this session's, and the tab's storage outlives it.
|
|
273
|
+
resetNotifications();
|
|
269
274
|
// Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
|
|
270
275
|
void destroyAllLocalIndexes().then(() => {
|
|
271
276
|
window.location.href = authServerUrl ?? "/";
|
|
@@ -284,6 +289,8 @@ export function AppChrome({
|
|
|
284
289
|
// Trusted signer pins read from contacts don't outlive the session either.
|
|
285
290
|
clearPinnedSignerCache();
|
|
286
291
|
clearAppearanceCache();
|
|
292
|
+
clearContactPhotoCache();
|
|
293
|
+
resetNotifications();
|
|
287
294
|
// Open compose windows save edits still waiting on their autosave debounce while the session is still
|
|
288
295
|
// valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
|
|
289
296
|
await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
|
|
@@ -306,6 +313,8 @@ export function AppChrome({
|
|
|
306
313
|
// they were, so there's nothing else useful to show — matching this app's other network-error
|
|
307
314
|
// handling, which surfaces via a full reload rather than an inline retry affordance.
|
|
308
315
|
} finally {
|
|
316
|
+
// What the impersonated user's session raised (their meetings, their errors) must not be left for the administrator to read.
|
|
317
|
+
resetNotifications();
|
|
309
318
|
window.location.href = "/admin";
|
|
310
319
|
}
|
|
311
320
|
}
|
|
@@ -10,7 +10,13 @@ import FormField from "../../../../lib/components/forms/FormField.js";
|
|
|
10
10
|
import { enrollKey, getKeyVault, PublicKey, VaultAlreadyInitializedError } from "../../../../lib/crypto/keyvaultApi.js";
|
|
11
11
|
import { UnopenableKeysNotice, unlockErrorMessage } from "./UnlockPromptProvider.js";
|
|
12
12
|
import { FrameTakeover } from "../../navigation/frameContext.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
MIN_PASSWORD_LENGTH,
|
|
15
|
+
RecoveryFollowUp,
|
|
16
|
+
RecoveryFollowUpModal,
|
|
17
|
+
UnlockModeToggle,
|
|
18
|
+
startRecoveryUnlock,
|
|
19
|
+
} from "./RecoveryCodeUnlock.js";
|
|
14
20
|
import {
|
|
15
21
|
ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE,
|
|
16
22
|
getUnlockedKeys,
|
|
@@ -23,8 +29,6 @@ import { exportPrivateKeyPkcs8, generateKeyPairWithCsr } from "../../../../lib/c
|
|
|
23
29
|
|
|
24
30
|
type Status = "checking" | "setup_password" | "enrolling" | "already_set_up" | "show_recovery_codes" | "unlock" | "unlocking" | "ready";
|
|
25
31
|
|
|
26
|
-
const MIN_PASSWORD_LENGTH = 8;
|
|
27
|
-
|
|
28
32
|
async function provisionEncryptionKey(
|
|
29
33
|
mailboxUid: string,
|
|
30
34
|
mailboxAddress: string,
|
|
@@ -299,7 +303,8 @@ export default function KeyEnrollmentGate({
|
|
|
299
303
|
<h1 className="text-lg font-bold mb-2">Protect your mailbox</h1>
|
|
300
304
|
<p className="text-sm text-text-muted mb-5">
|
|
301
305
|
Choose a password to protect your encryption keys. This is separate from your sign-in
|
|
302
|
-
password and is never sent to the server.
|
|
306
|
+
password and is never sent to the server. Make it {MIN_PASSWORD_LENGTH} or more characters: a
|
|
307
|
+
passphrase of several unrelated words is long enough and easy to remember.
|
|
303
308
|
</p>
|
|
304
309
|
{error && <Alert>{error}</Alert>}
|
|
305
310
|
<form onSubmit={handleSetPassword}>
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useEffect, useState } from "react";
|
|
6
6
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
7
|
-
import { MailboxAutoProvisionAliasOption, autoProvisionMailbox } from "../../../../lib/mail/mailApi.js";
|
|
7
|
+
import { MailboxAutoProvisionAliasOption, autoProvisionMailbox, stopImpersonating } from "../../../../lib/mail/mailApi.js";
|
|
8
8
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
9
9
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
10
10
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
11
11
|
import { FrameTakeover } from "../../navigation/frameContext.js";
|
|
12
12
|
|
|
13
|
-
type Status = "checking" | "needs_selection" | "creating" | "unavailable" | "retryable";
|
|
13
|
+
type Status = "checking" | "needs_selection" | "creating" | "unavailable" | "retryable" | "impersonating";
|
|
14
14
|
|
|
15
15
|
/** What the "Retry" screen says: the server couldn't read its provisioning policy (503) ... */
|
|
16
16
|
const RETRY_POLICY_TEXT = "We couldn\u2019t check whether a mailbox can be set up for you. Please try again.";
|
|
@@ -48,7 +48,7 @@ function unavailableReason(err: unknown): string | null {
|
|
|
48
48
|
* refused deliberately (a 4xx, e.g. "Automatic mailbox provisioning is not enabled.") so the caller and the
|
|
49
49
|
* administrator they ask can tell why - see `unavailableReason()`.
|
|
50
50
|
*/
|
|
51
|
-
export default function MailboxProvisioning() {
|
|
51
|
+
export default function MailboxProvisioning({ impersonating, impersonationBaseUrl }: { impersonating?: boolean; impersonationBaseUrl?: string }) {
|
|
52
52
|
const client = useApiClient();
|
|
53
53
|
const [status, setStatus] = useState<Status>("checking");
|
|
54
54
|
const [options, setOptions] = useState<MailboxAutoProvisionAliasOption[]>([]);
|
|
@@ -62,6 +62,12 @@ export default function MailboxProvisioning() {
|
|
|
62
62
|
const [attempt, setAttempt] = useState(0);
|
|
63
63
|
|
|
64
64
|
useEffect(() => {
|
|
65
|
+
if (impersonating) {
|
|
66
|
+
// 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
|
|
67
|
+
// (this takeover hides the frame's own "Return to admin" bar).
|
|
68
|
+
setStatus("impersonating");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
65
71
|
setStatus("checking");
|
|
66
72
|
autoProvisionMailbox(undefined, client)
|
|
67
73
|
.then((result) => {
|
|
@@ -93,6 +99,18 @@ export default function MailboxProvisioning() {
|
|
|
93
99
|
});
|
|
94
100
|
}, [attempt]);
|
|
95
101
|
|
|
102
|
+
const [returning, setReturning] = useState(false);
|
|
103
|
+
async function handleReturnToAdmin() {
|
|
104
|
+
setReturning(true);
|
|
105
|
+
try {
|
|
106
|
+
await stopImpersonating(impersonationBaseUrl ?? "");
|
|
107
|
+
} catch {
|
|
108
|
+
// Navigate either way, as the frame's own banner does: a failed call leaves the session as it was.
|
|
109
|
+
} finally {
|
|
110
|
+
window.location.href = "/admin";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
async function handleConfirm() {
|
|
97
115
|
// `selected` only ever takes a value from `options` itself (the initial default, or the
|
|
98
116
|
// <select>'s own onChange, which can only report one of its own rendered <option> values), so
|
|
@@ -136,6 +154,16 @@ export default function MailboxProvisioning() {
|
|
|
136
154
|
</Button>
|
|
137
155
|
</div>
|
|
138
156
|
);
|
|
157
|
+
} else if (status === "impersonating") {
|
|
158
|
+
content = (
|
|
159
|
+
<>
|
|
160
|
+
<h1 className="text-lg font-bold uppercase tracking-wide">No mailbox</h1>
|
|
161
|
+
<p className="text-sm text-text-muted">The user you are viewing as has no mailbox, and none is created for them while you are viewing as them.</p>
|
|
162
|
+
<Button type="button" onClick={() => void handleReturnToAdmin()} loading={returning} disabled={returning} className="!w-auto self-center">
|
|
163
|
+
Return to admin
|
|
164
|
+
</Button>
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
139
167
|
} else if (status === "retryable") {
|
|
140
168
|
content = (
|
|
141
169
|
<>
|
|
@@ -26,8 +26,10 @@ import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
|
26
26
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
27
27
|
import FormField from "../../../../lib/components/forms/FormField.js";
|
|
28
28
|
|
|
29
|
-
/** The same minimum every other "set an encryption password" form in this app uses.
|
|
30
|
-
|
|
29
|
+
/** The same minimum every other "set an encryption password" form in this app uses. The password is the only thing
|
|
30
|
+
* between an offline guesser holding the server's wrapped master key and the mailbox's private keys, so it is
|
|
31
|
+
* long: a passphrase of a few words clears it easily. */
|
|
32
|
+
export const MIN_PASSWORD_LENGTH = 12;
|
|
31
33
|
|
|
32
34
|
/** Where recovery codes are regenerated. */
|
|
33
35
|
export const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { useEffect, useState } from "react";
|
|
5
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
6
6
|
import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from "react-icons/hi2";
|
|
7
7
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
8
8
|
import { Message } from "../../../../lib/mail/mailApi.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 { ConversationSummary, 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";
|
|
@@ -120,17 +120,24 @@ export default function ConversationList({
|
|
|
120
120
|
const unlockEpoch = useUnlockEpoch();
|
|
121
121
|
// An encrypted conversation's row stands for its latest message, whose subject only decrypting can tell. Rows are decrypted as they are listed
|
|
122
122
|
// whenever the mailbox is unlocked (and again once it is), the same way the message list does.
|
|
123
|
+
// A few at a time (the list can hold hundreds), and none more once the list has been replaced by a newer one.
|
|
123
124
|
useEffect(() => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
let stale = false;
|
|
126
|
+
void mapWithConcurrency(conversations.filter(conversationLooksEncrypted), 4, async (conversation) => {
|
|
127
|
+
if (!stale) {
|
|
128
|
+
await decryptForDisplay(conversation.latestMessageUid, conversation.mailboxUid ?? mailboxUid);
|
|
127
129
|
}
|
|
128
|
-
}
|
|
130
|
+
});
|
|
131
|
+
return () => {
|
|
132
|
+
stale = true;
|
|
133
|
+
};
|
|
129
134
|
}, [conversations, mailboxUid, unlockEpoch]);
|
|
130
135
|
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
131
136
|
const [messagesById, setMessagesById] = useState<Record<string, Message[]>>({});
|
|
132
137
|
const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
|
|
133
138
|
const [errorsById, setErrorsById] = useState<Record<string, string>>({});
|
|
139
|
+
// The messages each cached thread was fetched for (its `messageUids`): a list that now says otherwise has the thread fetched again.
|
|
140
|
+
const fetchedForRef = useRef<Record<string, string>>({});
|
|
134
141
|
// 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.
|
|
135
142
|
const [answers, setAnswers] = useState<Record<string, Message["meetingResponse"]>>({});
|
|
136
143
|
|
|
@@ -138,6 +145,41 @@ export default function ConversationList({
|
|
|
138
145
|
setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
// 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
|
|
149
|
+
// 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
|
|
150
|
+
// that left the list is forgotten too.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const listed = new Set<string>();
|
|
153
|
+
const stale: string[] = [];
|
|
154
|
+
for (const conversation of conversations) {
|
|
155
|
+
const id = conversationRowKey(conversation);
|
|
156
|
+
listed.add(id);
|
|
157
|
+
const fetchedFor = fetchedForRef.current[id];
|
|
158
|
+
if (fetchedFor === undefined || fetchedFor === conversation.messageUids.join(",")) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (expanded.has(id)) {
|
|
162
|
+
fetchedForRef.current[id] = conversation.messageUids.join(",");
|
|
163
|
+
listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
|
|
164
|
+
.then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
|
|
165
|
+
.catch(() => undefined);
|
|
166
|
+
} else {
|
|
167
|
+
stale.push(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const id of Object.keys(fetchedForRef.current)) {
|
|
171
|
+
if (!listed.has(id)) {
|
|
172
|
+
stale.push(id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (stale.length > 0) {
|
|
176
|
+
for (const id of stale) {
|
|
177
|
+
delete fetchedForRef.current[id];
|
|
178
|
+
}
|
|
179
|
+
setMessagesById((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => !stale.includes(id))));
|
|
180
|
+
}
|
|
181
|
+
}, [conversations]);
|
|
182
|
+
|
|
141
183
|
function toggle(conversation: ListedConversation) {
|
|
142
184
|
const id = conversationRowKey(conversation);
|
|
143
185
|
setExpanded((prev) => {
|
|
@@ -158,6 +200,7 @@ export default function ConversationList({
|
|
|
158
200
|
delete next[id];
|
|
159
201
|
return next;
|
|
160
202
|
});
|
|
203
|
+
fetchedForRef.current[id] = conversation.messageUids.join(",");
|
|
161
204
|
listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId, {}, client)
|
|
162
205
|
.then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
|
|
163
206
|
.catch((err) =>
|