@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
|
@@ -151,6 +151,8 @@ export interface CalendarEvent {
|
|
|
151
151
|
|
|
152
152
|
const LIST_PAGE_SIZE = 500;
|
|
153
153
|
const LIST_CALENDAR_EVENTS_MAX_PAGES = 100;
|
|
154
|
+
/** Without an order, a page boundary can skip or repeat an event: the `uid` is a fixed one. */
|
|
155
|
+
const EVENT_SORT = JSON.stringify({ uid: "ASC" });
|
|
154
156
|
|
|
155
157
|
/**
|
|
156
158
|
* Lists every event in a folder (same "fetch the flat list, filter client-side" contract as
|
|
@@ -173,7 +175,7 @@ export async function listCalendarEvents(folderUid: string, client?: ApiClient):
|
|
|
173
175
|
for (let page = 0; page < LIST_CALENDAR_EVENTS_MAX_PAGES; page++) {
|
|
174
176
|
const batch = await withClient<CalendarEvent[]>(
|
|
175
177
|
client,
|
|
176
|
-
`/mail/calendar-events?${buildQuery({ limit: LIST_PAGE_SIZE, page }, { folderUid })}`,
|
|
178
|
+
`/mail/calendar-events?${buildQuery({ limit: LIST_PAGE_SIZE, page }, { folderUid, sort: EVENT_SORT })}`,
|
|
177
179
|
);
|
|
178
180
|
events.push(...batch);
|
|
179
181
|
if (batch.length < LIST_PAGE_SIZE) {
|
|
@@ -75,6 +75,37 @@ export function allOccurrences(events: CalendarEvent[], now: Date = new Date()):
|
|
|
75
75
|
return occurrencesOf(events, () => true, now);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The `count` occurrences of `chronological` (a series, in order of start, as `expandOccurrences()` returns it) that start nearest `now`, still in order. They
|
|
80
|
+
* are the run around `now`, so that is grown from it a side at a time (the nearer side first, the earlier one on a tie) rather than sorting the whole series.
|
|
81
|
+
*/
|
|
82
|
+
function nearest(chronological: CalendarOccurrence[], now: number, count: number): CalendarOccurrence[] {
|
|
83
|
+
const startOf = (o: CalendarOccurrence) => new Date(o.startDate).getTime();
|
|
84
|
+
// The first that starts at or after now.
|
|
85
|
+
let low = 0;
|
|
86
|
+
let high = chronological.length;
|
|
87
|
+
while (low < high) {
|
|
88
|
+
const middle = (low + high) >> 1;
|
|
89
|
+
if (startOf(chronological[middle]) < now) {
|
|
90
|
+
low = middle + 1;
|
|
91
|
+
} else {
|
|
92
|
+
high = middle;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let from = low;
|
|
96
|
+
let to = low;
|
|
97
|
+
while (to - from < count) {
|
|
98
|
+
const before = from > 0 ? now - startOf(chronological[from - 1]) : Infinity;
|
|
99
|
+
const after = to < chronological.length ? startOf(chronological[to]) - now : Infinity;
|
|
100
|
+
if (before <= after) {
|
|
101
|
+
from--;
|
|
102
|
+
} else {
|
|
103
|
+
to++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return chronological.slice(from, to);
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
/** The chronological occurrences of the `events` that `include` accepts (see `searchOccurrences()` for how a series is bounded). */
|
|
79
110
|
function occurrencesOf(events: CalendarEvent[], include: (event: CalendarEvent) => boolean, now: Date): CalendarOccurrence[] {
|
|
80
111
|
const from = addYears(now, -SEARCH_WINDOW_YEARS);
|
|
@@ -84,16 +115,25 @@ function occurrencesOf(events: CalendarEvent[], include: (event: CalendarEvent)
|
|
|
84
115
|
if (!include(event)) {
|
|
85
116
|
continue;
|
|
86
117
|
}
|
|
118
|
+
const once = (): CalendarOccurrence[] =>
|
|
119
|
+
expandOccurrences(
|
|
120
|
+
{ ...event, recurrenceRule: undefined },
|
|
121
|
+
new Date(new Date(event.startDate).getTime() - 1),
|
|
122
|
+
new Date(new Date(event.endDate).getTime() + 1),
|
|
123
|
+
);
|
|
87
124
|
if (!event.recurrenceRule) {
|
|
88
|
-
found.push(...
|
|
125
|
+
found.push(...once());
|
|
89
126
|
continue;
|
|
90
127
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
128
|
+
let occurrences: CalendarOccurrence[];
|
|
129
|
+
try {
|
|
130
|
+
occurrences = expandOccurrences(event, from, to);
|
|
131
|
+
} catch {
|
|
132
|
+
// One event whose rule or dates cannot be expanded must not fail the whole search or agenda: it counts once, as the stored event.
|
|
133
|
+
found.push(...once());
|
|
134
|
+
continue;
|
|
95
135
|
}
|
|
96
|
-
found.push(...occurrences);
|
|
136
|
+
found.push(...(occurrences.length > SEARCH_MAX_OCCURRENCES_PER_SERIES ? nearest(occurrences, now.getTime(), SEARCH_MAX_OCCURRENCES_PER_SERIES) : occurrences));
|
|
97
137
|
}
|
|
98
138
|
return found.sort(byStart);
|
|
99
139
|
}
|
|
@@ -78,18 +78,24 @@ export interface PersonAvailability {
|
|
|
78
78
|
/** What a person's calendar says about a stretch of time. `"unknown"` is any person the server did not (or would not) tell about - never free. */
|
|
79
79
|
export type AvailabilityState = "free" | "tentative" | "busy" | "unknown";
|
|
80
80
|
|
|
81
|
-
/** The response's results with their times as milliseconds (a person who isn't `"available"` has no blocks whatever the server sent).
|
|
81
|
+
/** The response's results with their times as milliseconds (a person who isn't `"available"` has no blocks whatever the server sent). A window
|
|
82
|
+
* whose start or end can't be read is busy for all time - never silently free, which is what comparing `NaN` would make it. */
|
|
82
83
|
export function availabilityOf(response: FreeBusyResponse): PersonAvailability[] {
|
|
83
84
|
return response.results.map((result) => ({
|
|
84
85
|
address: result.address,
|
|
85
86
|
status: result.status,
|
|
86
|
-
busy:
|
|
87
|
-
result.status === "available"
|
|
88
|
-
? result.busy.map((window) => ({ startMs: Date.parse(window.start), endMs: Date.parse(window.end), tentative: window.tentative }))
|
|
89
|
-
: [],
|
|
87
|
+
busy: result.status === "available" ? result.busy.map(toBusyBlock) : [],
|
|
90
88
|
}));
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
function toBusyBlock(window: { start: string; end: string; tentative: boolean }): BusyBlock {
|
|
92
|
+
const startMs = Date.parse(window.start);
|
|
93
|
+
const endMs = Date.parse(window.end);
|
|
94
|
+
return Number.isFinite(startMs) && Number.isFinite(endMs)
|
|
95
|
+
? { startMs, endMs, tentative: window.tentative }
|
|
96
|
+
: { startMs: Number.NEGATIVE_INFINITY, endMs: Number.POSITIVE_INFINITY, tentative: window.tentative };
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
/** Whether a block overlaps the stretch from `startMs` to `endMs` (touching ends do not overlap). */
|
|
94
100
|
function overlaps(block: BusyBlock, startMs: number, endMs: number): boolean {
|
|
95
101
|
return block.startMs < endMs && block.endMs > startMs;
|
|
@@ -165,6 +171,8 @@ export interface SuggestTimesOptions {
|
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
const DEFAULT_STEP_MS = 30 * 60_000;
|
|
174
|
+
/** The finest step `suggestTimes()` takes: a zero or negative one would never advance, a tiny one would test millions of slots. */
|
|
175
|
+
const MIN_STEP_MS = 60_000;
|
|
168
176
|
const DEFAULT_SUGGESTIONS = 5;
|
|
169
177
|
|
|
170
178
|
/**
|
|
@@ -177,7 +185,7 @@ export function suggestTimes(people: PersonAvailability[], options: SuggestTimes
|
|
|
177
185
|
if (known.length === 0 || options.durationMs <= 0) {
|
|
178
186
|
return [];
|
|
179
187
|
}
|
|
180
|
-
const step = options.stepMs
|
|
188
|
+
const step = Number.isFinite(options.stepMs) ? Math.max(MIN_STEP_MS, options.stepMs!) : DEFAULT_STEP_MS;
|
|
181
189
|
const limit = options.limit ?? DEFAULT_SUGGESTIONS;
|
|
182
190
|
const found: number[] = [];
|
|
183
191
|
for (const window of [...options.windows].sort((a, b) => a.startMs - b.startMs)) {
|
|
@@ -50,10 +50,38 @@ const WEEKDAY_OBJECT: Record<WeekdayCode, Weekday> = {
|
|
|
50
50
|
SU: RRule.SU,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/** A rule's interval as `rrule` can take it: a whole number of at least 1. A zero, negative, fractional or missing one (an
|
|
54
|
+
* imported or hand-edited rule) would make `rrule` loop forever, or expand to nothing, so it counts as the default 1. */
|
|
55
|
+
function normalizedInterval(interval: number): number {
|
|
56
|
+
return Number.isFinite(interval) && interval >= 1 ? Math.floor(interval) : 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The most days each month (January first) can ever have, counting February as a leap year's. */
|
|
60
|
+
const MAX_DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 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
|
|
64
|
+
* 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
|
|
65
|
+
* looking for one, which freezes the tab for seconds each time such an event is drawn.
|
|
66
|
+
*/
|
|
67
|
+
function assertSatisfiable(rule: RecurrenceRule): void {
|
|
68
|
+
const months = rule.byMonth ?? [];
|
|
69
|
+
const days = rule.byMonthDay ?? [];
|
|
70
|
+
if (months.some((m) => !Number.isInteger(m) || m < 1 || m > 12)) {
|
|
71
|
+
throw new Error("The recurrence rule has a month that does not exist");
|
|
72
|
+
}
|
|
73
|
+
if (days.some((d) => !Number.isInteger(d) || d === 0 || Math.abs(d) > 31)) {
|
|
74
|
+
throw new Error("The recurrence rule has a day of the month that does not exist");
|
|
75
|
+
}
|
|
76
|
+
if (months.length > 0 && days.length > 0 && !months.some((m) => days.some((d) => Math.abs(d) <= MAX_DAYS_IN_MONTH[m - 1]))) {
|
|
77
|
+
throw new Error("The recurrence rule names days that none of its months have");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
function createRRule(rule: RecurrenceRule, dtstart: Date, until: Date | null, tzid: string | null): RRuleNS.RRule {
|
|
54
82
|
return new RRule({
|
|
55
83
|
freq: FREQ_MAP[rule.freq],
|
|
56
|
-
interval: rule.interval,
|
|
84
|
+
interval: normalizedInterval(rule.interval),
|
|
57
85
|
dtstart,
|
|
58
86
|
tzid,
|
|
59
87
|
byweekday: rule.byDay?.map((code) => WEEKDAY_OBJECT[code]),
|
|
@@ -199,6 +227,21 @@ export function fromEventWallClock(wallMs: number, timezone: string | undefined,
|
|
|
199
227
|
return (allDay ? UTC_ZONE : resolveZone(timezone)).fromWall(wallMs);
|
|
200
228
|
}
|
|
201
229
|
|
|
230
|
+
/**
|
|
231
|
+
* `startWall` moved forward by whole repeat periods to just before `from`, for a daily or weekly rule that has no `count`: `rrule`
|
|
232
|
+
* walks every occurrence from the start, so a series that began centuries ago would cost a hundred thousand steps each time it is
|
|
233
|
+
* drawn. A multiple of the period lands on the same days, so the occurrences from `from` on are exactly those of the original.
|
|
234
|
+
* Anything else is returned as it is.
|
|
235
|
+
*/
|
|
236
|
+
function advancedStart(rule: RecurrenceRule, startWall: number, from: number): number {
|
|
237
|
+
if (rule.count !== undefined || (rule.freq !== "daily" && rule.freq !== "weekly")) {
|
|
238
|
+
return startWall;
|
|
239
|
+
}
|
|
240
|
+
const period = normalizedInterval(rule.interval) * (rule.freq === "weekly" ? 7 : 1) * MS_PER_DAY;
|
|
241
|
+
const periods = Math.floor((from - startWall) / period) - 1;
|
|
242
|
+
return periods > 0 ? startWall + periods * period : startWall;
|
|
243
|
+
}
|
|
244
|
+
|
|
202
245
|
/**
|
|
203
246
|
* Expands `event` into every occurrence whose interval overlaps `[rangeStart, rangeEnd]`. A
|
|
204
247
|
* non-recurring event yields itself (in a one-element array) if it overlaps, or `[]` otherwise —
|
|
@@ -223,22 +266,23 @@ export function expandOccurrences(event: CalendarEvent, rangeStart: Date, rangeE
|
|
|
223
266
|
return overlaps ? [{ ...event, occurrenceKey: event.uid, isRecurringOccurrence: false }] : [];
|
|
224
267
|
}
|
|
225
268
|
|
|
269
|
+
assertSatisfiable(event.recurrenceRule);
|
|
226
270
|
const zone = event.allDay ? UTC_ZONE : resolveZone(event.timezone);
|
|
227
271
|
const startWall = zone.toWall(start.getTime());
|
|
228
272
|
// The duration in wall-clock terms, so an occurrence also keeps its local end time across DST.
|
|
229
273
|
const wallDurationMs = zone.toWall(end.getTime()) - startWall;
|
|
230
274
|
const { until } = event.recurrenceRule;
|
|
231
|
-
const rule = createRRule(
|
|
232
|
-
event.recurrenceRule,
|
|
233
|
-
new Date(startWall),
|
|
234
|
-
until ? new Date(zone.toWall(new Date(until).getTime())) : null,
|
|
235
|
-
null,
|
|
236
|
-
);
|
|
237
275
|
// Query in the wall-clock frame, widened by one occurrence's duration (so an occurrence that started
|
|
238
276
|
// before `rangeStart` but is still in progress isn't missed) plus a day on each side to absorb the
|
|
239
277
|
// zone's offset; the exact instant-based overlap filter at the end trims the excess.
|
|
240
278
|
const queryStart = new Date(zone.toWall(rangeStart.getTime()) - wallDurationMs - MS_PER_DAY);
|
|
241
279
|
const queryEnd = new Date(zone.toWall(rangeEnd.getTime()) + MS_PER_DAY);
|
|
280
|
+
const rule = createRRule(
|
|
281
|
+
event.recurrenceRule,
|
|
282
|
+
new Date(advancedStart(event.recurrenceRule, startWall, queryStart.getTime())),
|
|
283
|
+
until ? new Date(zone.toWall(new Date(until).getTime())) : null,
|
|
284
|
+
null,
|
|
285
|
+
);
|
|
242
286
|
const occurrenceStarts = rule
|
|
243
287
|
.between(queryStart, queryEnd, true)
|
|
244
288
|
.map((occWall) => ({ start: new Date(zone.fromWall(occWall.getTime())), wall: occWall.getTime() }));
|
|
@@ -264,7 +308,14 @@ export function expandOccurrences(event: CalendarEvent, rangeStart: Date, rangeE
|
|
|
264
308
|
|
|
265
309
|
/** Expands every event in `events` and flattens the result — the usual entry point for a grid view. */
|
|
266
310
|
export function expandAllOccurrences(events: CalendarEvent[], rangeStart: Date, rangeEnd: Date): CalendarOccurrence[] {
|
|
267
|
-
return events.flatMap((event) =>
|
|
311
|
+
return events.flatMap((event) => {
|
|
312
|
+
try {
|
|
313
|
+
return expandOccurrences(event, rangeStart, rangeEnd);
|
|
314
|
+
} catch {
|
|
315
|
+
// One event whose rule or dates cannot be expanded must not blank the whole grid: it is shown once, as the stored event.
|
|
316
|
+
return expandOccurrences({ ...event, recurrenceRule: undefined }, rangeStart, rangeEnd);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
268
319
|
}
|
|
269
320
|
|
|
270
321
|
/** A human-readable summary of `rule` (e.g. "every 2 weeks on Monday, Wednesday until Jan 1, 2027"). */
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
8
|
+
/** 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. */
|
|
9
|
+
const MIDNIGHT_SLACK_MS = 1000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 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
|
|
13
|
+
* 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
|
|
14
|
+
* 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`).
|
|
15
|
+
* Every re-read sets the timer again from the real clock, so a timer that fired a day late cannot leave the key behind.
|
|
16
|
+
*/
|
|
17
|
+
export function useDayKey(): string {
|
|
18
|
+
const [dayKey, setDayKey] = useState(() => format(new Date(), "yyyy-MM-dd"));
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
21
|
+
// Re-reads the day and sets the timer for the midnight after it, replacing any that is pending.
|
|
22
|
+
function arm() {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
setDayKey(format(now, "yyyy-MM-dd"));
|
|
26
|
+
timer = setTimeout(arm, addDays(startOfDay(now), 1).getTime() - now.getTime() + MIDNIGHT_SLACK_MS);
|
|
27
|
+
}
|
|
28
|
+
function onVisible() {
|
|
29
|
+
if (document.visibilityState === "visible") {
|
|
30
|
+
arm();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
arm();
|
|
34
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
35
|
+
window.addEventListener("focus", arm);
|
|
36
|
+
window.addEventListener("pageshow", arm);
|
|
37
|
+
return () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
40
|
+
window.removeEventListener("focus", arm);
|
|
41
|
+
window.removeEventListener("pageshow", arm);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
return dayKey;
|
|
45
|
+
}
|
|
@@ -2,16 +2,20 @@
|
|
|
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";
|
|
6
|
-
import { gravatarEnabled, gravatarUrl } from "../../contacts/gravatar.js";
|
|
5
|
+
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { gravatarEnabled, gravatarUrl, isGravatarMissing, markGravatarMissing, subscribeGravatarPreference } from "../../contacts/gravatar.js";
|
|
7
7
|
|
|
8
8
|
export interface ContactAvatarProps {
|
|
9
9
|
displayName: string;
|
|
10
10
|
size?: number;
|
|
11
11
|
/** The contact's own picture (see `contactPhotoUrl()`); shown in preference to anything else. */
|
|
12
12
|
photoUrl?: string;
|
|
13
|
-
/** The contact's address: without a picture of their own, its Gravatar is shown when there is one (and the reader
|
|
13
|
+
/** 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). */
|
|
14
14
|
email?: string;
|
|
15
|
+
/** 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. */
|
|
16
|
+
photoPending?: boolean;
|
|
17
|
+
/** Called once, when the avatar scrolls into view - for a picture that is only fetched by then. */
|
|
18
|
+
onVisible?: () => void;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/** A fixed palette of background colors, cycled by a hash of the contact's name, for the initials shown when there is no picture. */
|
|
@@ -39,21 +43,47 @@ function colorOf(displayName: string): string {
|
|
|
39
43
|
* 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`
|
|
40
44
|
* is given), else initials on a color that is deterministic per name. A picture that fails to load falls through to the next.
|
|
41
45
|
*/
|
|
42
|
-
export default function ContactAvatar({ displayName, size = 32, photoUrl, email }: ContactAvatarProps) {
|
|
46
|
+
export default function ContactAvatar({ displayName, size = 32, photoUrl, email, photoPending, onVisible }: ContactAvatarProps) {
|
|
43
47
|
const [gravatar, setGravatar] = useState<string | undefined>();
|
|
44
48
|
const [failed, setFailed] = useState<string[]>([]);
|
|
49
|
+
const enabled = useSyncExternalStore(subscribeGravatarPreference, gravatarEnabled, () => false);
|
|
50
|
+
// Without IntersectionObserver there is no telling what is on screen, so look up at once.
|
|
51
|
+
const [visible, setVisible] = useState(() => typeof IntersectionObserver === "undefined");
|
|
52
|
+
const placeholder = useRef<HTMLSpanElement>(null);
|
|
53
|
+
const ownPicture = photoPending || (!!photoUrl && !failed.includes(photoUrl));
|
|
54
|
+
|
|
55
|
+
// A new picture gets a fresh start, whatever failed to load before.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setFailed((list) => (list.length ? [] : list));
|
|
58
|
+
}, [photoUrl]);
|
|
59
|
+
|
|
60
|
+
// 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.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const element = placeholder.current;
|
|
63
|
+
if (visible || !element || !(onVisible || (enabled && email && !ownPicture))) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const observer = new IntersectionObserver((entries) => {
|
|
67
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
68
|
+
setVisible(true);
|
|
69
|
+
onVisible?.();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
observer.observe(element);
|
|
73
|
+
return () => observer.disconnect();
|
|
74
|
+
}, [visible, enabled, email, ownPicture, onVisible]);
|
|
45
75
|
|
|
46
76
|
useEffect(() => {
|
|
47
77
|
setGravatar(undefined);
|
|
48
|
-
if (!email || !
|
|
78
|
+
if (!email || !enabled || !visible || ownPicture) {
|
|
49
79
|
return;
|
|
50
80
|
}
|
|
51
81
|
let cancelled = false;
|
|
52
|
-
void gravatarUrl(email, size * 2).then((url) => !cancelled && setGravatar(url));
|
|
82
|
+
void gravatarUrl(email, size * 2).then((url) => !cancelled && setGravatar(url && !isGravatarMissing(url) ? url : undefined));
|
|
53
83
|
return () => {
|
|
54
84
|
cancelled = true;
|
|
55
85
|
};
|
|
56
|
-
}, [email, size]);
|
|
86
|
+
}, [email, size, enabled, visible, ownPicture]);
|
|
57
87
|
|
|
58
88
|
const src = [photoUrl, gravatar].find((url) => url && !failed.includes(url));
|
|
59
89
|
if (src) {
|
|
@@ -65,7 +95,14 @@ export default function ContactAvatar({ displayName, size = 32, photoUrl, email
|
|
|
65
95
|
width={size}
|
|
66
96
|
height={size}
|
|
67
97
|
referrerPolicy="no-referrer"
|
|
68
|
-
|
|
98
|
+
loading="lazy"
|
|
99
|
+
decoding="async"
|
|
100
|
+
onError={() => {
|
|
101
|
+
if (src === gravatar) {
|
|
102
|
+
markGravatarMissing(src);
|
|
103
|
+
}
|
|
104
|
+
setFailed((list) => [...list, src]);
|
|
105
|
+
}}
|
|
69
106
|
className="inline-block rounded-full object-cover shrink-0"
|
|
70
107
|
style={{ width: size, height: size }}
|
|
71
108
|
/>
|
|
@@ -73,6 +110,7 @@ export default function ContactAvatar({ displayName, size = 32, photoUrl, email
|
|
|
73
110
|
}
|
|
74
111
|
return (
|
|
75
112
|
<span
|
|
113
|
+
ref={placeholder}
|
|
76
114
|
aria-hidden="true"
|
|
77
115
|
style={{ width: size, height: size, backgroundColor: colorOf(displayName), fontSize: size * 0.4 }}
|
|
78
116
|
className="inline-flex items-center justify-center rounded-full text-white font-semibold shrink-0"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useCallback, useState } from "react";
|
|
6
|
+
import type { Contact } from "../../contacts/contactsApi.js";
|
|
7
|
+
import { useContactPhoto } from "../../contacts/useContactPhotoSrc.js";
|
|
8
|
+
import { useApiClient } from "../../util/apiClientContext.js";
|
|
9
|
+
import ContactAvatar, { type ContactAvatarProps } from "./ContactAvatar.js";
|
|
10
|
+
|
|
11
|
+
export interface ContactPhotoAvatarProps extends Omit<ContactAvatarProps, "photoUrl"> {
|
|
12
|
+
/** The stored contact whose own picture is shown, if any. */
|
|
13
|
+
contact?: Pick<Contact, "uid" | "version" | "photoBlobKey">;
|
|
14
|
+
/** 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. */
|
|
15
|
+
lazy?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A `ContactAvatar` showing a stored contact's own picture the way the current app can load it - see `useContactPhotoSrc()`. */
|
|
19
|
+
export default function ContactPhotoAvatar({ contact, lazy, ...rest }: ContactPhotoAvatarProps) {
|
|
20
|
+
// Without IntersectionObserver there is no telling what is on screen, so the picture is fetched at once.
|
|
21
|
+
const [visible, setVisible] = useState(() => !lazy || typeof IntersectionObserver === "undefined");
|
|
22
|
+
const show = useCallback(() => setVisible(true), []);
|
|
23
|
+
const { src, failed } = useContactPhoto(contact, useApiClient(), visible);
|
|
24
|
+
// 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.
|
|
25
|
+
return <ContactAvatar {...rest} photoUrl={src} photoPending={!!contact?.photoBlobKey && !src && !failed} onVisible={visible ? undefined : show} />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
6
|
+
import Button from "../buttons/Button.js";
|
|
7
|
+
import Alert from "../feedback/Alert.js";
|
|
8
|
+
import Modal from "../overlays/Modal.js";
|
|
9
|
+
|
|
10
|
+
export interface CameraCaptureModalProps {
|
|
11
|
+
/** Called with the captured picture (a JPEG of the frame on screen); the camera is already released. The caller closes the dialog. */
|
|
12
|
+
onCapture: (file: File) => void;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
/** Also told why the camera could not be used or the picture not captured (the dialog shows it itself too). */
|
|
15
|
+
onError?: (message: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const CAMERA_BLOCKED_MESSAGE = "Camera access was blocked - allow it in your browser's settings, or choose Upload file.";
|
|
19
|
+
export const CAMERA_MISSING_MESSAGE = "No camera was found on this device - choose Upload file instead.";
|
|
20
|
+
export const CAMERA_FAILED_MESSAGE = "The camera could not be started - choose Upload file instead.";
|
|
21
|
+
|
|
22
|
+
/** The friendly reason a `getUserMedia()` rejection gives (`NotAllowedError`/`SecurityError`: the user or the page's policy said no; `NotFoundError`/`OverconstrainedError`: no camera). */
|
|
23
|
+
function cameraErrorMessage(err: unknown): string {
|
|
24
|
+
const name = (err as { name?: string } | null)?.name;
|
|
25
|
+
if (name === "NotAllowedError" || name === "SecurityError") {
|
|
26
|
+
return CAMERA_BLOCKED_MESSAGE;
|
|
27
|
+
}
|
|
28
|
+
if (name === "NotFoundError" || name === "OverconstrainedError" || name === "DevicesNotFoundError") {
|
|
29
|
+
return CAMERA_MISSING_MESSAGE;
|
|
30
|
+
}
|
|
31
|
+
return CAMERA_FAILED_MESSAGE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 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
|
|
36
|
+
* 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
|
|
37
|
+
* stopped again when the dialog goes away (captured, cancelled, or the page left), which is what turns the camera's light off.
|
|
38
|
+
*/
|
|
39
|
+
export default function CameraCaptureModal({ onCapture, onClose, onError }: CameraCaptureModalProps) {
|
|
40
|
+
const video = useRef<HTMLVideoElement>(null);
|
|
41
|
+
const stream = useRef<MediaStream | null>(null);
|
|
42
|
+
// 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.
|
|
43
|
+
const open = useRef(true);
|
|
44
|
+
const [ready, setReady] = useState(false);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
// From pressing Capture until the frame is encoded: a second press then would hand the caller a second picture.
|
|
47
|
+
const [capturing, setCapturing] = useState(false);
|
|
48
|
+
|
|
49
|
+
function fail(message: string) {
|
|
50
|
+
setError(message);
|
|
51
|
+
onError?.(message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stop() {
|
|
55
|
+
stream.current?.getTracks().forEach((track) => track.stop());
|
|
56
|
+
stream.current = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 1280 } } }).then(
|
|
62
|
+
(media) => {
|
|
63
|
+
if (cancelled) {
|
|
64
|
+
media.getTracks().forEach((track) => track.stop());
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
stream.current = media;
|
|
68
|
+
video.current!.srcObject = media;
|
|
69
|
+
},
|
|
70
|
+
(err) => !cancelled && fail(cameraErrorMessage(err)),
|
|
71
|
+
);
|
|
72
|
+
return () => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
open.current = false;
|
|
75
|
+
stop();
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
function close() {
|
|
80
|
+
open.current = false;
|
|
81
|
+
stop();
|
|
82
|
+
onClose();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function capture() {
|
|
86
|
+
setCapturing(true);
|
|
87
|
+
const element = video.current!;
|
|
88
|
+
const canvas = document.createElement("canvas");
|
|
89
|
+
canvas.width = element.videoWidth;
|
|
90
|
+
canvas.height = element.videoHeight;
|
|
91
|
+
canvas.getContext("2d")!.drawImage(element, 0, 0, canvas.width, canvas.height);
|
|
92
|
+
canvas.toBlob(
|
|
93
|
+
(blob) => {
|
|
94
|
+
if (!open.current) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!blob) {
|
|
98
|
+
setCapturing(false);
|
|
99
|
+
fail("The picture could not be captured - try again.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
stop();
|
|
103
|
+
onCapture(new File([blob], "photo.jpg", { type: "image/jpeg" }));
|
|
104
|
+
},
|
|
105
|
+
"image/jpeg",
|
|
106
|
+
0.92,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Modal open onClose={close} title="Take photo">
|
|
112
|
+
{error && <Alert>{error}</Alert>}
|
|
113
|
+
{/* Mirrored like a mirror, which is what looking at yourself expects; the picture that is kept is not. */}
|
|
114
|
+
<video
|
|
115
|
+
ref={video}
|
|
116
|
+
autoPlay
|
|
117
|
+
playsInline
|
|
118
|
+
muted
|
|
119
|
+
aria-label="Camera preview"
|
|
120
|
+
onLoadedMetadata={() => setReady(true)}
|
|
121
|
+
className={`w-full aspect-square object-cover rounded-sm bg-black -scale-x-100 ${error ? "hidden" : ""}`}
|
|
122
|
+
/>
|
|
123
|
+
<div className="mt-5 flex justify-end gap-2">
|
|
124
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={close}>
|
|
125
|
+
Cancel
|
|
126
|
+
</Button>
|
|
127
|
+
<Button type="button" className="!w-auto" disabled={!ready || !!error || capturing} onClick={capture}>
|
|
128
|
+
Capture
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</Modal>
|
|
132
|
+
);
|
|
133
|
+
}
|