@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
|
@@ -42,10 +42,35 @@ const WEEKDAY_OBJECT = {
|
|
|
42
42
|
SA: RRule.SA,
|
|
43
43
|
SU: RRule.SU,
|
|
44
44
|
};
|
|
45
|
+
/** A rule's interval as `rrule` can take it: a whole number of at least 1. A zero, negative, fractional or missing one (an
|
|
46
|
+
* imported or hand-edited rule) would make `rrule` loop forever, or expand to nothing, so it counts as the default 1. */
|
|
47
|
+
function normalizedInterval(interval) {
|
|
48
|
+
return Number.isFinite(interval) && interval >= 1 ? Math.floor(interval) : 1;
|
|
49
|
+
}
|
|
50
|
+
/** The most days each month (January first) can ever have, counting February as a leap year's. */
|
|
51
|
+
const MAX_DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
52
|
+
/**
|
|
53
|
+
* Throws for a rule that can never produce an occurrence: a month outside 1-12, a day of the month outside 1-31 (or -1 to -31), or days
|
|
54
|
+
* that no listed month has (the 30th of February). `rrule` has no way to know that, so `between()` walks every day up to the year 9999
|
|
55
|
+
* looking for one, which freezes the tab for seconds each time such an event is drawn.
|
|
56
|
+
*/
|
|
57
|
+
function assertSatisfiable(rule) {
|
|
58
|
+
const months = rule.byMonth ?? [];
|
|
59
|
+
const days = rule.byMonthDay ?? [];
|
|
60
|
+
if (months.some((m) => !Number.isInteger(m) || m < 1 || m > 12)) {
|
|
61
|
+
throw new Error("The recurrence rule has a month that does not exist");
|
|
62
|
+
}
|
|
63
|
+
if (days.some((d) => !Number.isInteger(d) || d === 0 || Math.abs(d) > 31)) {
|
|
64
|
+
throw new Error("The recurrence rule has a day of the month that does not exist");
|
|
65
|
+
}
|
|
66
|
+
if (months.length > 0 && days.length > 0 && !months.some((m) => days.some((d) => Math.abs(d) <= MAX_DAYS_IN_MONTH[m - 1]))) {
|
|
67
|
+
throw new Error("The recurrence rule names days that none of its months have");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
45
70
|
function createRRule(rule, dtstart, until, tzid) {
|
|
46
71
|
return new RRule({
|
|
47
72
|
freq: FREQ_MAP[rule.freq],
|
|
48
|
-
interval: rule.interval,
|
|
73
|
+
interval: normalizedInterval(rule.interval),
|
|
49
74
|
dtstart,
|
|
50
75
|
tzid,
|
|
51
76
|
byweekday: rule.byDay?.map((code) => WEEKDAY_OBJECT[code]),
|
|
@@ -150,6 +175,20 @@ export function toEventWallClock(instantMs, timezone, allDay) {
|
|
|
150
175
|
export function fromEventWallClock(wallMs, timezone, allDay) {
|
|
151
176
|
return (allDay ? UTC_ZONE : resolveZone(timezone)).fromWall(wallMs);
|
|
152
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* `startWall` moved forward by whole repeat periods to just before `from`, for a daily or weekly rule that has no `count`: `rrule`
|
|
180
|
+
* walks every occurrence from the start, so a series that began centuries ago would cost a hundred thousand steps each time it is
|
|
181
|
+
* drawn. A multiple of the period lands on the same days, so the occurrences from `from` on are exactly those of the original.
|
|
182
|
+
* Anything else is returned as it is.
|
|
183
|
+
*/
|
|
184
|
+
function advancedStart(rule, startWall, from) {
|
|
185
|
+
if (rule.count !== undefined || (rule.freq !== "daily" && rule.freq !== "weekly")) {
|
|
186
|
+
return startWall;
|
|
187
|
+
}
|
|
188
|
+
const period = normalizedInterval(rule.interval) * (rule.freq === "weekly" ? 7 : 1) * MS_PER_DAY;
|
|
189
|
+
const periods = Math.floor((from - startWall) / period) - 1;
|
|
190
|
+
return periods > 0 ? startWall + periods * period : startWall;
|
|
191
|
+
}
|
|
153
192
|
/**
|
|
154
193
|
* Expands `event` into every occurrence whose interval overlaps `[rangeStart, rangeEnd]`. A
|
|
155
194
|
* non-recurring event yields itself (in a one-element array) if it overlaps, or `[]` otherwise —
|
|
@@ -172,17 +211,18 @@ export function expandOccurrences(event, rangeStart, rangeEnd) {
|
|
|
172
211
|
const overlaps = start < rangeEnd && end > rangeStart;
|
|
173
212
|
return overlaps ? [{ ...event, occurrenceKey: event.uid, isRecurringOccurrence: false }] : [];
|
|
174
213
|
}
|
|
214
|
+
assertSatisfiable(event.recurrenceRule);
|
|
175
215
|
const zone = event.allDay ? UTC_ZONE : resolveZone(event.timezone);
|
|
176
216
|
const startWall = zone.toWall(start.getTime());
|
|
177
217
|
// The duration in wall-clock terms, so an occurrence also keeps its local end time across DST.
|
|
178
218
|
const wallDurationMs = zone.toWall(end.getTime()) - startWall;
|
|
179
219
|
const { until } = event.recurrenceRule;
|
|
180
|
-
const rule = createRRule(event.recurrenceRule, new Date(startWall), until ? new Date(zone.toWall(new Date(until).getTime())) : null, null);
|
|
181
220
|
// Query in the wall-clock frame, widened by one occurrence's duration (so an occurrence that started
|
|
182
221
|
// before `rangeStart` but is still in progress isn't missed) plus a day on each side to absorb the
|
|
183
222
|
// zone's offset; the exact instant-based overlap filter at the end trims the excess.
|
|
184
223
|
const queryStart = new Date(zone.toWall(rangeStart.getTime()) - wallDurationMs - MS_PER_DAY);
|
|
185
224
|
const queryEnd = new Date(zone.toWall(rangeEnd.getTime()) + MS_PER_DAY);
|
|
225
|
+
const rule = createRRule(event.recurrenceRule, new Date(advancedStart(event.recurrenceRule, startWall, queryStart.getTime())), until ? new Date(zone.toWall(new Date(until).getTime())) : null, null);
|
|
186
226
|
const occurrenceStarts = rule
|
|
187
227
|
.between(queryStart, queryEnd, true)
|
|
188
228
|
.map((occWall) => ({ start: new Date(zone.fromWall(occWall.getTime())), wall: occWall.getTime() }));
|
|
@@ -206,7 +246,15 @@ export function expandOccurrences(event, rangeStart, rangeEnd) {
|
|
|
206
246
|
}
|
|
207
247
|
/** Expands every event in `events` and flattens the result — the usual entry point for a grid view. */
|
|
208
248
|
export function expandAllOccurrences(events, rangeStart, rangeEnd) {
|
|
209
|
-
return events.flatMap((event) =>
|
|
249
|
+
return events.flatMap((event) => {
|
|
250
|
+
try {
|
|
251
|
+
return expandOccurrences(event, rangeStart, rangeEnd);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// One event whose rule or dates cannot be expanded must not blank the whole grid: it is shown once, as the stored event.
|
|
255
|
+
return expandOccurrences({ ...event, recurrenceRule: undefined }, rangeStart, rangeEnd);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
210
258
|
}
|
|
211
259
|
/** A human-readable summary of `rule` (e.g. "every 2 weeks on Monday, Wednesday until Jan 1, 2027"). */
|
|
212
260
|
export function describeRecurrence(rule) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Today's local date as `YYYY-MM-DD`, kept current: it ticks over at the next local midnight, so a page left open overnight stops treating yesterday
|
|
3
|
+
* as today (use it as a dependency of whatever was worked out from "now"). It is also re-read when the tab becomes visible again or the window is focused or shown
|
|
4
|
+
* again, as a hidden tab's timers are throttled or suspended (a sleeping laptop, which may wake with the tab still "visible" and so with no `visibilitychange`).
|
|
5
|
+
* Every re-read sets the timer again from the real clock, so a timer that fired a day late cannot leave the key behind.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useDayKey(): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { addDays, format, startOfDay } from "date-fns";
|
|
7
|
+
/** How long past local midnight the key is re-read, so a timer that fires a hair early does not land on the day that is ending. */
|
|
8
|
+
const MIDNIGHT_SLACK_MS = 1000;
|
|
9
|
+
/**
|
|
10
|
+
* Today's local date as `YYYY-MM-DD`, kept current: it ticks over at the next local midnight, so a page left open overnight stops treating yesterday
|
|
11
|
+
* as today (use it as a dependency of whatever was worked out from "now"). It is also re-read when the tab becomes visible again or the window is focused or shown
|
|
12
|
+
* again, as a hidden tab's timers are throttled or suspended (a sleeping laptop, which may wake with the tab still "visible" and so with no `visibilitychange`).
|
|
13
|
+
* Every re-read sets the timer again from the real clock, so a timer that fired a day late cannot leave the key behind.
|
|
14
|
+
*/
|
|
15
|
+
export function useDayKey() {
|
|
16
|
+
const [dayKey, setDayKey] = useState(() => format(new Date(), "yyyy-MM-dd"));
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let timer;
|
|
19
|
+
// Re-reads the day and sets the timer for the midnight after it, replacing any that is pending.
|
|
20
|
+
function arm() {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
const now = new Date();
|
|
23
|
+
setDayKey(format(now, "yyyy-MM-dd"));
|
|
24
|
+
timer = setTimeout(arm, addDays(startOfDay(now), 1).getTime() - now.getTime() + MIDNIGHT_SLACK_MS);
|
|
25
|
+
}
|
|
26
|
+
function onVisible() {
|
|
27
|
+
if (document.visibilityState === "visible") {
|
|
28
|
+
arm();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
arm();
|
|
32
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
33
|
+
window.addEventListener("focus", arm);
|
|
34
|
+
window.addEventListener("pageshow", arm);
|
|
35
|
+
return () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
38
|
+
window.removeEventListener("focus", arm);
|
|
39
|
+
window.removeEventListener("pageshow", arm);
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
return dayKey;
|
|
43
|
+
}
|
|
@@ -4,11 +4,15 @@ export interface ContactAvatarProps {
|
|
|
4
4
|
size?: number;
|
|
5
5
|
/** The contact's own picture (see `contactPhotoUrl()`); shown in preference to anything else. */
|
|
6
6
|
photoUrl?: string;
|
|
7
|
-
/** The contact's address: without a picture of their own, its Gravatar is shown when there is one (and the reader
|
|
7
|
+
/** The contact's address: without a picture of their own, its Gravatar is shown when there is one (and the reader turned that on in Settings > Privacy). */
|
|
8
8
|
email?: string;
|
|
9
|
+
/** The contact's own picture exists but has not arrived (it is being fetched): Gravatar is not asked about an address whose owner has a picture of their own. */
|
|
10
|
+
photoPending?: boolean;
|
|
11
|
+
/** Called once, when the avatar scrolls into view - for a picture that is only fetched by then. */
|
|
12
|
+
onVisible?: () => void;
|
|
9
13
|
}
|
|
10
14
|
/**
|
|
11
15
|
* A small circular avatar for a contact, matching Outlook People's: the contact's own picture when they have one, else their Gravatar (when `email`
|
|
12
16
|
* is given), else initials on a color that is deterministic per name. A picture that fails to load falls through to the next.
|
|
13
17
|
*/
|
|
14
|
-
export default function ContactAvatar({ displayName, size, photoUrl, email }: ContactAvatarProps): React.JSX.Element;
|
|
18
|
+
export default function ContactAvatar({ displayName, size, photoUrl, email, photoPending, onVisible }: ContactAvatarProps): React.JSX.Element;
|
|
@@ -3,8 +3,8 @@ import { jsx as _jsx } 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";
|
|
7
|
-
import { gravatarEnabled, gravatarUrl } from "../../contacts/gravatar.js";
|
|
6
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
7
|
+
import { gravatarEnabled, gravatarUrl, isGravatarMissing, markGravatarMissing, subscribeGravatarPreference } from "../../contacts/gravatar.js";
|
|
8
8
|
/** A fixed palette of background colors, cycled by a hash of the contact's name, for the initials shown when there is no picture. */
|
|
9
9
|
const PALETTE = ["#7c3aed", "#2563eb", "#0891b2", "#059669", "#d97706", "#dc2626", "#db2777", "#4f46e5"];
|
|
10
10
|
function initialsOf(displayName) {
|
|
@@ -27,23 +27,52 @@ function colorOf(displayName) {
|
|
|
27
27
|
* A small circular avatar for a contact, matching Outlook People's: the contact's own picture when they have one, else their Gravatar (when `email`
|
|
28
28
|
* is given), else initials on a color that is deterministic per name. A picture that fails to load falls through to the next.
|
|
29
29
|
*/
|
|
30
|
-
export default function ContactAvatar({ displayName, size = 32, photoUrl, email }) {
|
|
30
|
+
export default function ContactAvatar({ displayName, size = 32, photoUrl, email, photoPending, onVisible }) {
|
|
31
31
|
const [gravatar, setGravatar] = useState();
|
|
32
32
|
const [failed, setFailed] = useState([]);
|
|
33
|
+
const enabled = useSyncExternalStore(subscribeGravatarPreference, gravatarEnabled, () => false);
|
|
34
|
+
// Without IntersectionObserver there is no telling what is on screen, so look up at once.
|
|
35
|
+
const [visible, setVisible] = useState(() => typeof IntersectionObserver === "undefined");
|
|
36
|
+
const placeholder = useRef(null);
|
|
37
|
+
const ownPicture = photoPending || (!!photoUrl && !failed.includes(photoUrl));
|
|
38
|
+
// A new picture gets a fresh start, whatever failed to load before.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setFailed((list) => (list.length ? [] : list));
|
|
41
|
+
}, [photoUrl]);
|
|
42
|
+
// Only an avatar that is on screen (and has no picture of its own to show) asks Gravatar, so a long list does not send a request per row.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const element = placeholder.current;
|
|
45
|
+
if (visible || !element || !(onVisible || (enabled && email && !ownPicture))) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const observer = new IntersectionObserver((entries) => {
|
|
49
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
50
|
+
setVisible(true);
|
|
51
|
+
onVisible?.();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
observer.observe(element);
|
|
55
|
+
return () => observer.disconnect();
|
|
56
|
+
}, [visible, enabled, email, ownPicture, onVisible]);
|
|
33
57
|
useEffect(() => {
|
|
34
58
|
setGravatar(undefined);
|
|
35
|
-
if (!email || !
|
|
59
|
+
if (!email || !enabled || !visible || ownPicture) {
|
|
36
60
|
return;
|
|
37
61
|
}
|
|
38
62
|
let cancelled = false;
|
|
39
|
-
void gravatarUrl(email, size * 2).then((url) => !cancelled && setGravatar(url));
|
|
63
|
+
void gravatarUrl(email, size * 2).then((url) => !cancelled && setGravatar(url && !isGravatarMissing(url) ? url : undefined));
|
|
40
64
|
return () => {
|
|
41
65
|
cancelled = true;
|
|
42
66
|
};
|
|
43
|
-
}, [email, size]);
|
|
67
|
+
}, [email, size, enabled, visible, ownPicture]);
|
|
44
68
|
const src = [photoUrl, gravatar].find((url) => url && !failed.includes(url));
|
|
45
69
|
if (src) {
|
|
46
|
-
return (_jsx("img", { src: src, alt: "", "aria-hidden": "true", width: size, height: size, referrerPolicy: "no-referrer",
|
|
70
|
+
return (_jsx("img", { src: src, alt: "", "aria-hidden": "true", width: size, height: size, referrerPolicy: "no-referrer", loading: "lazy", decoding: "async", onError: () => {
|
|
71
|
+
if (src === gravatar) {
|
|
72
|
+
markGravatarMissing(src);
|
|
73
|
+
}
|
|
74
|
+
setFailed((list) => [...list, src]);
|
|
75
|
+
}, className: "inline-block rounded-full object-cover shrink-0", style: { width: size, height: size } }));
|
|
47
76
|
}
|
|
48
|
-
return (_jsx("span", { "aria-hidden": "true", style: { width: size, height: size, backgroundColor: colorOf(displayName), fontSize: size * 0.4 }, className: "inline-flex items-center justify-center rounded-full text-white font-semibold shrink-0", children: initialsOf(displayName) }));
|
|
77
|
+
return (_jsx("span", { ref: placeholder, "aria-hidden": "true", style: { width: size, height: size, backgroundColor: colorOf(displayName), fontSize: size * 0.4 }, className: "inline-flex items-center justify-center rounded-full text-white font-semibold shrink-0", children: initialsOf(displayName) }));
|
|
49
78
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Contact } from "../../contacts/contactsApi.js";
|
|
3
|
+
import { type ContactAvatarProps } from "./ContactAvatar.js";
|
|
4
|
+
export interface ContactPhotoAvatarProps extends Omit<ContactAvatarProps, "photoUrl"> {
|
|
5
|
+
/** The stored contact whose own picture is shown, if any. */
|
|
6
|
+
contact?: Pick<Contact, "uid" | "version" | "photoBlobKey">;
|
|
7
|
+
/** For a row of a long list: a picture that has to be fetched (a token-authenticated app) is only fetched once the avatar scrolls into view. */
|
|
8
|
+
lazy?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** A `ContactAvatar` showing a stored contact's own picture the way the current app can load it - see `useContactPhotoSrc()`. */
|
|
11
|
+
export default function ContactPhotoAvatar({ contact, lazy, ...rest }: ContactPhotoAvatarProps): React.JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useCallback, useState } from "react";
|
|
7
|
+
import { useContactPhoto } from "../../contacts/useContactPhotoSrc.js";
|
|
8
|
+
import { useApiClient } from "../../util/apiClientContext.js";
|
|
9
|
+
import ContactAvatar from "./ContactAvatar.js";
|
|
10
|
+
/** A `ContactAvatar` showing a stored contact's own picture the way the current app can load it - see `useContactPhotoSrc()`. */
|
|
11
|
+
export default function ContactPhotoAvatar({ contact, lazy, ...rest }) {
|
|
12
|
+
// Without IntersectionObserver there is no telling what is on screen, so the picture is fetched at once.
|
|
13
|
+
const [visible, setVisible] = useState(() => !lazy || typeof IntersectionObserver === "undefined");
|
|
14
|
+
const show = useCallback(() => setVisible(true), []);
|
|
15
|
+
const { src, failed } = useContactPhoto(contact, useApiClient(), visible);
|
|
16
|
+
// A picture that exists but is not here yet - not fetched yet or on its way - keeps Gravatar from being asked; one that could not be fetched does not.
|
|
17
|
+
return _jsx(ContactAvatar, { ...rest, photoUrl: src, photoPending: !!contact?.photoBlobKey && !src && !failed, onVisible: visible ? undefined : show });
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface CameraCaptureModalProps {
|
|
3
|
+
/** Called with the captured picture (a JPEG of the frame on screen); the camera is already released. The caller closes the dialog. */
|
|
4
|
+
onCapture: (file: File) => void;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
/** Also told why the camera could not be used or the picture not captured (the dialog shows it itself too). */
|
|
7
|
+
onError?: (message: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const CAMERA_BLOCKED_MESSAGE = "Camera access was blocked - allow it in your browser's settings, or choose Upload file.";
|
|
10
|
+
export declare const CAMERA_MISSING_MESSAGE = "No camera was found on this device - choose Upload file instead.";
|
|
11
|
+
export declare const CAMERA_FAILED_MESSAGE = "The camera could not be started - choose Upload file instead.";
|
|
12
|
+
/**
|
|
13
|
+
* A dialog with the live picture of the device's camera (the front one where there is a choice) and a Capture button, for a computer with a webcam - a phone's own
|
|
14
|
+
* camera is reached through a file input's `capture` attribute instead. Asking for the camera is what makes the browser prompt for permission; every track is
|
|
15
|
+
* stopped again when the dialog goes away (captured, cancelled, or the page left), which is what turns the camera's light off.
|
|
16
|
+
*/
|
|
17
|
+
export default function CameraCaptureModal({ onCapture, onClose, onError }: CameraCaptureModalProps): React.JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import Button from "../buttons/Button.js";
|
|
8
|
+
import Alert from "../feedback/Alert.js";
|
|
9
|
+
import Modal from "../overlays/Modal.js";
|
|
10
|
+
export const CAMERA_BLOCKED_MESSAGE = "Camera access was blocked - allow it in your browser's settings, or choose Upload file.";
|
|
11
|
+
export const CAMERA_MISSING_MESSAGE = "No camera was found on this device - choose Upload file instead.";
|
|
12
|
+
export const CAMERA_FAILED_MESSAGE = "The camera could not be started - choose Upload file instead.";
|
|
13
|
+
/** The friendly reason a `getUserMedia()` rejection gives (`NotAllowedError`/`SecurityError`: the user or the page's policy said no; `NotFoundError`/`OverconstrainedError`: no camera). */
|
|
14
|
+
function cameraErrorMessage(err) {
|
|
15
|
+
const name = err?.name;
|
|
16
|
+
if (name === "NotAllowedError" || name === "SecurityError") {
|
|
17
|
+
return CAMERA_BLOCKED_MESSAGE;
|
|
18
|
+
}
|
|
19
|
+
if (name === "NotFoundError" || name === "OverconstrainedError" || name === "DevicesNotFoundError") {
|
|
20
|
+
return CAMERA_MISSING_MESSAGE;
|
|
21
|
+
}
|
|
22
|
+
return CAMERA_FAILED_MESSAGE;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A dialog with the live picture of the device's camera (the front one where there is a choice) and a Capture button, for a computer with a webcam - a phone's own
|
|
26
|
+
* camera is reached through a file input's `capture` attribute instead. Asking for the camera is what makes the browser prompt for permission; every track is
|
|
27
|
+
* stopped again when the dialog goes away (captured, cancelled, or the page left), which is what turns the camera's light off.
|
|
28
|
+
*/
|
|
29
|
+
export default function CameraCaptureModal({ onCapture, onClose, onError }) {
|
|
30
|
+
const video = useRef(null);
|
|
31
|
+
const stream = useRef(null);
|
|
32
|
+
// Cleared when the dialog is cancelled or gone: a frame still being encoded then must not reach `onCapture` of a caller that has moved on.
|
|
33
|
+
const open = useRef(true);
|
|
34
|
+
const [ready, setReady] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
// From pressing Capture until the frame is encoded: a second press then would hand the caller a second picture.
|
|
37
|
+
const [capturing, setCapturing] = useState(false);
|
|
38
|
+
function fail(message) {
|
|
39
|
+
setError(message);
|
|
40
|
+
onError?.(message);
|
|
41
|
+
}
|
|
42
|
+
function stop() {
|
|
43
|
+
stream.current?.getTracks().forEach((track) => track.stop());
|
|
44
|
+
stream.current = null;
|
|
45
|
+
}
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 1280 } } }).then((media) => {
|
|
49
|
+
if (cancelled) {
|
|
50
|
+
media.getTracks().forEach((track) => track.stop());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
stream.current = media;
|
|
54
|
+
video.current.srcObject = media;
|
|
55
|
+
}, (err) => !cancelled && fail(cameraErrorMessage(err)));
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
open.current = false;
|
|
59
|
+
stop();
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
function close() {
|
|
63
|
+
open.current = false;
|
|
64
|
+
stop();
|
|
65
|
+
onClose();
|
|
66
|
+
}
|
|
67
|
+
function capture() {
|
|
68
|
+
setCapturing(true);
|
|
69
|
+
const element = video.current;
|
|
70
|
+
const canvas = document.createElement("canvas");
|
|
71
|
+
canvas.width = element.videoWidth;
|
|
72
|
+
canvas.height = element.videoHeight;
|
|
73
|
+
canvas.getContext("2d").drawImage(element, 0, 0, canvas.width, canvas.height);
|
|
74
|
+
canvas.toBlob((blob) => {
|
|
75
|
+
if (!open.current) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!blob) {
|
|
79
|
+
setCapturing(false);
|
|
80
|
+
fail("The picture could not be captured - try again.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
stop();
|
|
84
|
+
onCapture(new File([blob], "photo.jpg", { type: "image/jpeg" }));
|
|
85
|
+
}, "image/jpeg", 0.92);
|
|
86
|
+
}
|
|
87
|
+
return (_jsxs(Modal, { open: true, onClose: close, title: "Take photo", children: [error && _jsx(Alert, { children: error }), _jsx("video", { ref: video, autoPlay: true, playsInline: true, muted: true, "aria-label": "Camera preview", onLoadedMetadata: () => setReady(true), className: `w-full aspect-square object-cover rounded-sm bg-black -scale-x-100 ${error ? "hidden" : ""}` }), _jsxs("div", { className: "mt-5 flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: close, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", disabled: !ready || !!error || capturing, onClick: capture, children: "Capture" })] })] }));
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ImageEditBadgeProps {
|
|
3
|
+
/** Where the badge sits on the picture it edits: the lower right of an avatar, the upper right of a banner. */
|
|
4
|
+
position: "bottom-right" | "top-right";
|
|
5
|
+
/** Whether there is a picture to remove: "Remove photo" is in the menu only then. */
|
|
6
|
+
hasImage: boolean;
|
|
7
|
+
/** `"sm"` (20px) for a picture too small for the default 28px badge, such as a 48px avatar. */
|
|
8
|
+
size?: "sm" | "md";
|
|
9
|
+
/** Disables the badge while a chosen picture is being worked on. */
|
|
10
|
+
busy?: boolean;
|
|
11
|
+
/** The badge's accessible name, which also names its menu ("Change contact photo", "Change banner"). */
|
|
12
|
+
label: string;
|
|
13
|
+
/** The accessible name of the (hidden) file input "Upload file" opens. The camera input is named "<this> (camera)". */
|
|
14
|
+
fileInputLabel?: string;
|
|
15
|
+
/** Called with the picture as it was chosen or captured; the caller makes it fit what it stores. */
|
|
16
|
+
onFile: (file: File) => void;
|
|
17
|
+
/** Called by "Remove photo". Without it there is no such row. */
|
|
18
|
+
onRemove?: () => void;
|
|
19
|
+
/** Told why the camera could not be used (the camera dialog shows it too). */
|
|
20
|
+
onError?: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A round camera badge laid over the corner of a picture - an avatar or a banner - that opens a menu to change it: "Upload file", "Take photo" and, when there is a
|
|
24
|
+
* picture, "Remove photo". The badge is `position: absolute`, so the caller wraps the picture in a `relative` element (`relative inline-block` for an avatar) and puts this
|
|
25
|
+
* inside it. The menu is `MenuButton`'s: arrow keys, Enter, Escape and a click outside close it, focus returns to the badge. "Take photo" opens the device's camera app through
|
|
26
|
+
* an input's `capture` on a phone or tablet, and a live preview dialog (`getUserMedia`) on a computer; it is disabled, with a title that says why, where there is neither.
|
|
27
|
+
*/
|
|
28
|
+
export default function ImageEditBadge({ position, hasImage, busy, size, label, fileInputLabel, onFile, onRemove, onError }: ImageEditBadgeProps): React.JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useRef, useState } from "react";
|
|
7
|
+
import { HiArrowUpTray, HiCamera, HiTrash } from "react-icons/hi2";
|
|
8
|
+
import MenuButton from "../menus/MenuButton.js";
|
|
9
|
+
import CameraCaptureModal from "./CameraCaptureModal.js";
|
|
10
|
+
/** How "Take photo" reaches the camera on this device, or why it cannot: a touch device (a phone or tablet) hands the picking to its own camera app through an input's
|
|
11
|
+
* `capture`; anything else with `getUserMedia` gets the live preview dialog. */
|
|
12
|
+
function cameraAccess() {
|
|
13
|
+
if (typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches) {
|
|
14
|
+
return { mode: "capture" };
|
|
15
|
+
}
|
|
16
|
+
if (typeof navigator.mediaDevices?.getUserMedia === "function") {
|
|
17
|
+
return { mode: "preview" };
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
unavailable: window.isSecureContext === false ? "The camera needs a secure (https) connection." : "This browser cannot use the camera - choose Upload file instead.",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const POSITIONS = { "bottom-right": "bottom-0 right-0", "top-right": "top-2 right-2" };
|
|
24
|
+
const SIZES = { sm: { box: "h-5 w-5", icon: 12 }, md: { box: "h-7 w-7", icon: 14 } };
|
|
25
|
+
/**
|
|
26
|
+
* A round camera badge laid over the corner of a picture - an avatar or a banner - that opens a menu to change it: "Upload file", "Take photo" and, when there is a
|
|
27
|
+
* picture, "Remove photo". The badge is `position: absolute`, so the caller wraps the picture in a `relative` element (`relative inline-block` for an avatar) and puts this
|
|
28
|
+
* inside it. The menu is `MenuButton`'s: arrow keys, Enter, Escape and a click outside close it, focus returns to the badge. "Take photo" opens the device's camera app through
|
|
29
|
+
* an input's `capture` on a phone or tablet, and a live preview dialog (`getUserMedia`) on a computer; it is disabled, with a title that says why, where there is neither.
|
|
30
|
+
*/
|
|
31
|
+
export default function ImageEditBadge({ position, hasImage, busy, size = "md", label, fileInputLabel = "Image file", onFile, onRemove, onError }) {
|
|
32
|
+
const fileInput = useRef(null);
|
|
33
|
+
const captureInput = useRef(null);
|
|
34
|
+
const [cameraOpen, setCameraOpen] = useState(false);
|
|
35
|
+
function fileChanged(e) {
|
|
36
|
+
const file = e.target.files?.[0];
|
|
37
|
+
if (file) {
|
|
38
|
+
onFile(file);
|
|
39
|
+
}
|
|
40
|
+
// The same file can be chosen again.
|
|
41
|
+
e.target.value = "";
|
|
42
|
+
}
|
|
43
|
+
const camera = cameraAccess();
|
|
44
|
+
const items = [
|
|
45
|
+
{ key: "upload", label: "Upload file", icon: _jsx(HiArrowUpTray, { size: 14 }), onSelect: () => fileInput.current?.click() },
|
|
46
|
+
{
|
|
47
|
+
key: "camera",
|
|
48
|
+
label: "Take photo",
|
|
49
|
+
icon: _jsx(HiCamera, { size: 14 }),
|
|
50
|
+
disabled: !camera.mode,
|
|
51
|
+
title: camera.unavailable,
|
|
52
|
+
onSelect: () => (camera.mode === "capture" ? captureInput.current?.click() : setCameraOpen(true)),
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
if (hasImage && onRemove) {
|
|
56
|
+
items.push({ key: "remove", label: "Remove photo", icon: _jsx(HiTrash, { size: 14 }), onSelect: onRemove });
|
|
57
|
+
}
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsx(MenuButton, { iconOnly: true, label: label, "aria-label": label, title: label, icon: _jsx(HiCamera, { size: SIZES[size].icon, "aria-hidden": "true" }), disabled: busy, width: 192, sections: [{ key: "image", items }], className: `absolute ${POSITIONS[position]} flex ${SIZES[size].box} items-center justify-center rounded-full bg-black/70 text-white ring-2 ring-surface hover:bg-black/80 disabled:opacity-50` }), _jsx("input", { ref: fileInput, type: "file", accept: "image/*", "aria-label": fileInputLabel, className: "sr-only", tabIndex: -1, onChange: fileChanged }), _jsx("input", { ref: captureInput, type: "file", accept: "image/*", capture: "user", "aria-label": `${fileInputLabel} (camera)`, className: "sr-only", tabIndex: -1, onChange: fileChanged }), cameraOpen && (_jsx(CameraCaptureModal, { onClose: () => setCameraOpen(false), onError: onError, onCapture: (file) => {
|
|
59
|
+
setCameraOpen(false);
|
|
60
|
+
onFile(file);
|
|
61
|
+
} }))] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
/** What every row has. `checked` turns it into a radio/checkbox item (the caller says which via `role`) and
|
|
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 {};
|