@rapidmx/web-client 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +21 -2
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { addDays } from "date-fns";
|
|
6
|
+
|
|
7
|
+
/** Where things sit in the hour grids of the Week, Work Week, Day and Split views: 24 rows of one clock hour each. */
|
|
8
|
+
export const HOUR_HEIGHT_PX = 48;
|
|
9
|
+
export const SLOT_MINUTES = 30;
|
|
10
|
+
export const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
11
|
+
/** The least height a block is drawn with, so a very short event can still be seen and clicked. */
|
|
12
|
+
export const MIN_BLOCK_HEIGHT_PX = 16;
|
|
13
|
+
|
|
14
|
+
/** Minutes after midnight on the wall clock (the local zone) at `ms`. */
|
|
15
|
+
function wallMinutes(ms: number): number {
|
|
16
|
+
const date = new Date(ms);
|
|
17
|
+
return date.getHours() * 60 + date.getMinutes() + date.getSeconds() / 60;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The start of the `index`th half-hour row of the day beginning at `dayStart`, by the wall clock - the rows are labelled with clock hours, so on the
|
|
22
|
+
* day a daylight-saving change makes 23 or 25 hours long the row labelled 10 AM must still be 10:00, not ten hours after midnight. (A row whose
|
|
23
|
+
* clock time does not exist that day is the next one that does.)
|
|
24
|
+
*/
|
|
25
|
+
export function slotStart(dayStart: Date, index: number): Date {
|
|
26
|
+
return new Date(dayStart.getFullYear(), dayStart.getMonth(), dayStart.getDate(), 0, index * SLOT_MINUTES);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Where a block for an event from `startMs` to `endMs` goes in the column of the day beginning at `dayStart`: a multi-day event is clipped to the day,
|
|
31
|
+
* and (`wholeDay`, for an all-day event) fills it. Positioned by wall-clock minute, like the rows, so a block lines up with its hour on a 23 or 25
|
|
32
|
+
* hour day too.
|
|
33
|
+
*/
|
|
34
|
+
export function blockSpan(startMs: number, endMs: number, dayStart: Date, wholeDay = false): { top: number; height: number } {
|
|
35
|
+
const dayEnd = addDays(dayStart, 1).getTime();
|
|
36
|
+
const from = wholeDay || startMs < dayStart.getTime() ? 0 : wallMinutes(startMs);
|
|
37
|
+
const to = wholeDay || endMs >= dayEnd ? 24 * 60 : wallMinutes(endMs);
|
|
38
|
+
return {
|
|
39
|
+
top: (from / 60) * HOUR_HEIGHT_PX,
|
|
40
|
+
height: Math.max(((to - from) / 60) * HOUR_HEIGHT_PX, MIN_BLOCK_HEIGHT_PX),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One block's share of the width of its column: the lane it sits in, out of how many its overlapping neighbours need. */
|
|
45
|
+
export interface Lane {
|
|
46
|
+
lane: number;
|
|
47
|
+
lanes: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Splits a column's width between blocks that overlap in time, as Outlook and Google Calendar do, so none is hidden behind another: blocks that
|
|
52
|
+
* overlap (directly or through a chain of others) form a group, and within it each takes the first lane that is free when it starts. Every block of a
|
|
53
|
+
* group gets the group's lane count; a block that overlaps nothing is alone in one lane (the whole width). Results are in the order of `spans`.
|
|
54
|
+
*/
|
|
55
|
+
export function layoutLanes(spans: { top: number; height: number }[]): Lane[] {
|
|
56
|
+
const lanes: Lane[] = spans.map(() => ({ lane: 0, lanes: 1 }));
|
|
57
|
+
const order = spans.map((_, index) => index).sort((a, b) => spans[a].top - spans[b].top || spans[b].height - spans[a].height);
|
|
58
|
+
let group: number[] = [];
|
|
59
|
+
let groupEnd = Number.NEGATIVE_INFINITY;
|
|
60
|
+
let laneEnds: number[] = [];
|
|
61
|
+
const closeGroup = () => {
|
|
62
|
+
for (const index of group) {
|
|
63
|
+
lanes[index].lanes = laneEnds.length;
|
|
64
|
+
}
|
|
65
|
+
group = [];
|
|
66
|
+
laneEnds = [];
|
|
67
|
+
};
|
|
68
|
+
for (const index of order) {
|
|
69
|
+
const { top, height } = spans[index];
|
|
70
|
+
if (top >= groupEnd) {
|
|
71
|
+
closeGroup();
|
|
72
|
+
groupEnd = Number.NEGATIVE_INFINITY;
|
|
73
|
+
}
|
|
74
|
+
let lane = laneEnds.findIndex((end) => end <= top);
|
|
75
|
+
if (lane === -1) {
|
|
76
|
+
lane = laneEnds.length;
|
|
77
|
+
laneEnds.push(top + height);
|
|
78
|
+
} else {
|
|
79
|
+
laneEnds[lane] = top + height;
|
|
80
|
+
}
|
|
81
|
+
lanes[index].lane = lane;
|
|
82
|
+
group.push(index);
|
|
83
|
+
groupEnd = Math.max(groupEnd, top + height);
|
|
84
|
+
}
|
|
85
|
+
closeGroup();
|
|
86
|
+
return lanes;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The horizontal placement of a block in its column: the whole width less a 2px margin, or its lane's share of it. */
|
|
90
|
+
export function laneStyle({ lane, lanes }: Lane): { left: string | number; width?: string; right?: number } {
|
|
91
|
+
return lanes === 1
|
|
92
|
+
? { left: 2, right: 2 }
|
|
93
|
+
: { left: `calc(${(lane / lanes) * 100}% + 2px)`, width: `calc(${100 / lanes}% - 4px)` };
|
|
94
|
+
}
|
|
@@ -156,7 +156,7 @@ export default function CalendarShell({
|
|
|
156
156
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
157
157
|
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
158
158
|
if (userUid && status === "ready" && mailboxes.length === 0) {
|
|
159
|
-
return <MailboxProvisioning />;
|
|
159
|
+
return <MailboxProvisioning impersonating={impersonating} impersonationBaseUrl={impersonationBaseUrl} />;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
let inner: ReactNode = null;
|
|
@@ -13,8 +13,11 @@ const MS_PER_MINUTE = 60_000;
|
|
|
13
13
|
* event lasting an hour from there.
|
|
14
14
|
*/
|
|
15
15
|
export function nextHalfHour(now: Date): Date {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Rounded on the local clock: a zone at :30 or :45 from UTC (India, Nepal, Chatham) has its half hours mid-way between the epoch's.
|
|
17
|
+
const next = new Date(now);
|
|
18
|
+
const minutes = next.getMinutes() + (next.getSeconds() * 1000 + next.getMilliseconds()) / MS_PER_MINUTE;
|
|
19
|
+
next.setMinutes(Math.ceil(minutes / 30) * 30, 0, 0);
|
|
20
|
+
return next;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/** `HH:mm` (24 hour) as a 12 hour clock reading: "13:05" is "1:05 PM". */
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { ReactNode, useContext, useEffect, useRef, useState } from "react";
|
|
6
|
-
import { type Contact, type ContactAddressKind,
|
|
6
|
+
import { type Contact, type ContactAddressKind, setContactFavorite } from "../../../../lib/contacts/contactsApi.js";
|
|
7
7
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
8
|
-
import
|
|
8
|
+
import ContactPhotoAvatar from "../../../../lib/components/avatar/ContactPhotoAvatar.js";
|
|
9
|
+
import ContactPhotoEditor from "./ContactPhotoEditor.js";
|
|
9
10
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
10
11
|
import CopyIconButton from "../../../../lib/components/buttons/CopyIconButton.js";
|
|
11
12
|
import { HiOutlineEnvelope, HiOutlineUserPlus } from "react-icons/hi2";
|
|
@@ -17,6 +18,7 @@ import type { ParsedVCardContact } from "../../../../lib/contacts/vcard.js";
|
|
|
17
18
|
import { type Folder, type Mailbox, listMailboxes } from "../../../../lib/mail/mailApi.js";
|
|
18
19
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
19
20
|
import { MAILBOX_LIST_LIMIT, MailConnectionContext } from "../../mail/useMailConnection.js";
|
|
21
|
+
import { useMailboxUpdateAccess } from "../../mail/useMailboxUpdateAccess.js";
|
|
20
22
|
import { notify } from "../../notifications/store.js";
|
|
21
23
|
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
22
24
|
import { APP_HREFS } from "../../navigation/appHrefs.js";
|
|
@@ -187,6 +189,8 @@ export default function ContactCard({ participant, context = {}, onClose, userUi
|
|
|
187
189
|
|
|
188
190
|
const name = participantName(participant);
|
|
189
191
|
const stored = changed ?? created ?? (contact.status === "ready" ? contact.data : undefined);
|
|
192
|
+
// A contact in a mailbox shared view-only cannot get a new picture here (the server would refuse it).
|
|
193
|
+
const canEditPhoto = useMailboxUpdateAccess(mailboxes.status === "ready" ? mailboxes.data.find((mailbox) => mailbox.uid === stored?.mailboxUid) : undefined);
|
|
190
194
|
const external = mailboxes.status === "ready" && isExternalAddress(participant.address, ownDomains(mailboxes.data));
|
|
191
195
|
// What is known when there is no contact: the name, the address and the message's vCard.
|
|
192
196
|
const known = stored ?? contactInputFor(participant, card.status === "ready" ? card.data : undefined, { mailboxUid: "", folderUid: "" });
|
|
@@ -215,6 +219,9 @@ export default function ContactCard({ participant, context = {}, onClose, userUi
|
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
|
|
222
|
+
// Not while a picture is being changed: that goes against the version the star would replace.
|
|
223
|
+
const [photoBusy, setPhotoBusy] = useState(false);
|
|
224
|
+
|
|
218
225
|
async function handleToggleFavorite(current: Contact) {
|
|
219
226
|
try {
|
|
220
227
|
const next = await setContactFavorite(current, !current.favorite, client);
|
|
@@ -234,7 +241,11 @@ export default function ContactCard({ participant, context = {}, onClose, userUi
|
|
|
234
241
|
return (
|
|
235
242
|
<Modal open onClose={onClose} title={name}>
|
|
236
243
|
<div className="flex items-center gap-4">
|
|
237
|
-
|
|
244
|
+
{stored && canEditPhoto ? (
|
|
245
|
+
<ContactPhotoEditor contact={stored} displayName={name} size={72} email={participant.address} onChanged={setChanged} onBusyChange={setPhotoBusy} />
|
|
246
|
+
) : (
|
|
247
|
+
<ContactPhotoAvatar displayName={name} size={72} contact={stored} email={participant.address} />
|
|
248
|
+
)}
|
|
238
249
|
<div className="min-w-0 flex-1">
|
|
239
250
|
<p className="text-sm text-text-muted break-words">
|
|
240
251
|
{participant.address}
|
|
@@ -247,7 +258,7 @@ export default function ContactCard({ participant, context = {}, onClose, userUi
|
|
|
247
258
|
<p className="mt-1 text-sm break-words">{[known.jobTitle, known.company].filter(Boolean).join(", ")}</p>
|
|
248
259
|
)}
|
|
249
260
|
</div>
|
|
250
|
-
{stored && <FavoriteStarButton favorite={!!stored.favorite} onToggle={() => void handleToggleFavorite(stored)} className="shrink-0 self-start" />}
|
|
261
|
+
{stored && <FavoriteStarButton favorite={!!stored.favorite} disabled={photoBusy} onToggle={() => void handleToggleFavorite(stored)} className="shrink-0 self-start" />}
|
|
251
262
|
</div>
|
|
252
263
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
253
264
|
<Button type="button" variant="secondary" className={`${ACTION_CLASS} inline-flex items-center gap-1.5`} onClick={handleEmail}>
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useState } from "react";
|
|
6
|
-
import { Contact,
|
|
6
|
+
import { Contact, setContactFavorite } from "../../../../lib/contacts/contactsApi.js";
|
|
7
7
|
import CopyIconButton from "../../../../lib/components/buttons/CopyIconButton.js";
|
|
8
8
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
9
9
|
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
10
10
|
import FavoriteStarButton from "./FavoriteStarButton.js";
|
|
11
11
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
12
12
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
13
|
-
import
|
|
13
|
+
import ContactPhotoAvatar from "../../../../lib/components/avatar/ContactPhotoAvatar.js";
|
|
14
|
+
import ContactPhotoEditor from "./ContactPhotoEditor.js";
|
|
14
15
|
import KeyChangeReview from "./KeyChangeReview.js";
|
|
15
16
|
import { KEY_CHANGE_STALE_MESSAGE, formatDate, keyPinnedSince, revocationLabel } from "./contactKeys.js";
|
|
16
17
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
@@ -34,6 +35,8 @@ export interface ContactDetailPaneProps {
|
|
|
34
35
|
contact: Contact;
|
|
35
36
|
onEdit: () => void;
|
|
36
37
|
onDelete: () => void;
|
|
38
|
+
/** Whether the reader may change this contact: `false` for one in a mailbox shared with them view-only, which gets no Edit or Delete. Default `true`. */
|
|
39
|
+
canWrite?: boolean;
|
|
37
40
|
/** Present only on the mobile detail route — renders a "back to contacts" link above the header. Absent
|
|
38
41
|
* on the desktop inline pane, which never navigates away (selecting a different contact just swaps
|
|
39
42
|
* `contact` in place). */
|
|
@@ -43,8 +46,8 @@ export interface ContactDetailPaneProps {
|
|
|
43
46
|
onKeysChanged?: () => void;
|
|
44
47
|
/** Called with the contact after the star was toggled, so the caller shows the changed contact. */
|
|
45
48
|
onChanged?: (contact: Contact) => void;
|
|
46
|
-
/** `false` when the reader is known not to be able to change this contact
|
|
47
|
-
* when unknown (
|
|
49
|
+
/** `false` when the reader is known not to be able to change this contact (its key actions and the photo badge are hidden); `undefined`
|
|
50
|
+
* when unknown (the key actions are hidden after a 403; a refused photo change is a pop-up). */
|
|
48
51
|
canResolveKeys?: boolean;
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -53,7 +56,7 @@ export interface ContactDetailPaneProps {
|
|
|
53
56
|
* always visible alongside the contact list) and the mobile detail route
|
|
54
57
|
* (`apps/www/contacts/[uid].tsx`, a full page on its own reached by tapping a contact row).
|
|
55
58
|
*/
|
|
56
|
-
export default function ContactDetailPane({ contact, onEdit, onDelete, backHref, onKeysChanged, onChanged, canResolveKeys }: ContactDetailPaneProps) {
|
|
59
|
+
export default function ContactDetailPane({ contact, onEdit, onDelete, canWrite = true, backHref, onKeysChanged, onChanged, canResolveKeys }: ContactDetailPaneProps) {
|
|
57
60
|
const client = useApiClient();
|
|
58
61
|
// The outcome of the last key decision, kept per contact so selecting another contact doesn't carry it over.
|
|
59
62
|
const [keyNotice, setKeyNotice] = useState<{ contactUid: string; text: string } | null>(null);
|
|
@@ -61,6 +64,12 @@ export default function ContactDetailPane({ contact, onEdit, onDelete, backHref,
|
|
|
61
64
|
const keyConflicts = contact.keyConflicts ?? [];
|
|
62
65
|
const previousKeys = contact.previousKeys ?? [];
|
|
63
66
|
|
|
67
|
+
// Not while a picture is being changed: that goes against the version the star would replace.
|
|
68
|
+
const [photoBusy, setPhotoBusy] = useState(false);
|
|
69
|
+
// Delete asks first - it also drops the pinned signing keys this contact vouches with - and the question belongs to the contact it was asked about.
|
|
70
|
+
const [confirmingDeleteUid, setConfirmingDeleteUid] = useState<string | null>(null);
|
|
71
|
+
const confirmingDelete = confirmingDeleteUid === contact.uid;
|
|
72
|
+
|
|
64
73
|
async function handleToggleFavorite() {
|
|
65
74
|
try {
|
|
66
75
|
const changed = await setContactFavorite(contact, !contact.favorite, client);
|
|
@@ -86,11 +95,23 @@ export default function ContactDetailPane({ contact, onEdit, onDelete, backHref,
|
|
|
86
95
|
)}
|
|
87
96
|
<div className="flex items-start justify-between gap-4">
|
|
88
97
|
<div className="flex items-center gap-3">
|
|
89
|
-
|
|
98
|
+
{canResolveKeys === false ? (
|
|
99
|
+
<ContactPhotoAvatar displayName={contact.displayName} size={48} contact={contact} email={contact.emails[0]?.address} />
|
|
100
|
+
) : (
|
|
101
|
+
<ContactPhotoEditor
|
|
102
|
+
contact={contact}
|
|
103
|
+
displayName={contact.displayName}
|
|
104
|
+
size={48}
|
|
105
|
+
badgeSize="sm"
|
|
106
|
+
email={contact.emails[0]?.address}
|
|
107
|
+
onChanged={(changed) => onChanged?.(changed)}
|
|
108
|
+
onBusyChange={setPhotoBusy}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
90
111
|
<div>
|
|
91
112
|
<h1 className="text-xl font-bold tracking-tight">
|
|
92
113
|
{contact.displayName}
|
|
93
|
-
<FavoriteStarButton favorite={!!contact.favorite} onToggle={() => void handleToggleFavorite()} className="ml-1 align-middle" />
|
|
114
|
+
<FavoriteStarButton favorite={!!contact.favorite} disabled={photoBusy} onToggle={() => void handleToggleFavorite()} className="ml-1 align-middle" />
|
|
94
115
|
</h1>
|
|
95
116
|
{contact.jobTitle && contact.company && (
|
|
96
117
|
<p className="text-sm text-text-muted mt-0.5">
|
|
@@ -99,14 +120,32 @@ export default function ContactDetailPane({ contact, onEdit, onDelete, backHref,
|
|
|
99
120
|
)}
|
|
100
121
|
</div>
|
|
101
122
|
</div>
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
{canWrite && (
|
|
124
|
+
<div className="flex flex-wrap justify-end gap-2 shrink-0">
|
|
125
|
+
{confirmingDelete ? (
|
|
126
|
+
<>
|
|
127
|
+
<span role="alert" className="self-center text-sm">
|
|
128
|
+
Delete this contact?
|
|
129
|
+
</span>
|
|
130
|
+
<Button type="button" variant="secondary" className="!w-auto text-danger" onClick={onDelete}>
|
|
131
|
+
Yes, delete
|
|
132
|
+
</Button>
|
|
133
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingDeleteUid(null)}>
|
|
134
|
+
Keep
|
|
135
|
+
</Button>
|
|
136
|
+
</>
|
|
137
|
+
) : (
|
|
138
|
+
<>
|
|
139
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={onEdit}>
|
|
140
|
+
Edit
|
|
141
|
+
</Button>
|
|
142
|
+
<Button type="button" variant="secondary" className="!w-auto text-danger" onClick={() => setConfirmingDeleteUid(contact.uid)}>
|
|
143
|
+
Delete
|
|
144
|
+
</Button>
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
110
149
|
</div>
|
|
111
150
|
|
|
112
151
|
<div className="bg-surface border border-border rounded-md p-6 flex flex-col gap-4 text-sm">
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
ContactPatch,
|
|
12
12
|
ContactPhone,
|
|
13
13
|
ContactPostalAddress,
|
|
14
|
-
contactPhotoUrl,
|
|
15
14
|
createContact,
|
|
16
15
|
deleteContactPhoto,
|
|
16
|
+
getContact,
|
|
17
17
|
updateContact,
|
|
18
18
|
uploadContactPhoto,
|
|
19
19
|
} from "../../../../lib/contacts/contactsApi.js";
|
|
@@ -24,6 +24,7 @@ import FormField from "../../../../lib/components/forms/FormField.js";
|
|
|
24
24
|
import ContactPhotoField from "./ContactPhotoField.js";
|
|
25
25
|
import FavoriteStarButton from "./FavoriteStarButton.js";
|
|
26
26
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
27
|
+
import { useContactPhotoSrc } from "../../../../lib/contacts/useContactPhotoSrc.js";
|
|
27
28
|
import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
|
|
28
29
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
29
30
|
|
|
@@ -53,6 +54,7 @@ const ADDRESS_KINDS: ContactAddressKind[] = ["home", "work", "other"];
|
|
|
53
54
|
*/
|
|
54
55
|
export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes, onSaved, onCancel }: ContactFormProps) {
|
|
55
56
|
const client = useApiClient();
|
|
57
|
+
const currentPhotoUrl = useContactPhotoSrc(contact, client);
|
|
56
58
|
const [targetMailboxUid, setTargetMailboxUid] = useState(mailboxUid);
|
|
57
59
|
const [displayName, setDisplayName] = useState(contact?.displayName ?? "");
|
|
58
60
|
const [givenName, setGivenName] = useState(contact?.givenName ?? "");
|
|
@@ -70,8 +72,13 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
70
72
|
// A picture chosen (or the current one dropped) is applied once the contact itself is saved.
|
|
71
73
|
const [pickedPhoto, setPickedPhoto] = useState<File | null>(null);
|
|
72
74
|
const [removePhoto, setRemovePhoto] = useState(false);
|
|
75
|
+
// A chosen picture is being made small enough (see `ContactPhotoField`): the contact is not saved before it is ready.
|
|
76
|
+
const [preparingPhoto, setPreparingPhoto] = useState(false);
|
|
73
77
|
const [error, setError] = useState<string | null>(null);
|
|
74
78
|
const [saving, setSaving] = useState(false);
|
|
79
|
+
// The contact as stored, once it has been saved and only the picture is left to do (that is a second request, which can fail on its own):
|
|
80
|
+
// a retry must not create the contact again or send the version it started from. `key` is the form's content that was saved.
|
|
81
|
+
const [stored, setStored] = useState<{ contact: Contact; key: string } | null>(null);
|
|
75
82
|
|
|
76
83
|
function updateEmail(index: number, patch: Partial<ContactEmail>) {
|
|
77
84
|
setEmails((prev) => prev.map((e, i) => (i === index ? { ...e, ...patch } : e)));
|
|
@@ -109,7 +116,8 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
109
116
|
.filter(Boolean);
|
|
110
117
|
// An update merges only the fields in its body, so a field the user cleared is sent as an
|
|
111
118
|
// explicit `null` (an omitted/undefined one would keep its stored value); a create just omits it.
|
|
112
|
-
const
|
|
119
|
+
const existing = stored?.contact ?? contact;
|
|
120
|
+
const cleared = existing ? null : undefined;
|
|
113
121
|
const input = {
|
|
114
122
|
displayName: displayName.trim(),
|
|
115
123
|
givenName: givenName.trim() || cleared,
|
|
@@ -119,16 +127,21 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
119
127
|
notes: notes.trim() || cleared,
|
|
120
128
|
favorite,
|
|
121
129
|
categories,
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
// A row added with "+ Add" and left empty is not sent: the server refuses a contact with a blank address or number.
|
|
131
|
+
emails: emails.filter((email) => email.address.trim()).map((email) => ({ ...email, address: email.address.trim() })),
|
|
132
|
+
phones: phones.filter((phone) => phone.phoneNumber.trim()).map((phone) => ({ ...phone, phoneNumber: phone.phoneNumber.trim() })),
|
|
124
133
|
addresses,
|
|
125
134
|
};
|
|
135
|
+
const key = JSON.stringify([targetMailboxUid, input]);
|
|
126
136
|
let saved: Contact;
|
|
127
|
-
if (
|
|
137
|
+
if (stored && stored.key === key) {
|
|
138
|
+
// Stored exactly as it is now: only the picture is left.
|
|
139
|
+
saved = stored.contact;
|
|
140
|
+
} else if (existing) {
|
|
128
141
|
saved = await updateContact(
|
|
129
142
|
{
|
|
130
|
-
uid:
|
|
131
|
-
version:
|
|
143
|
+
uid: existing.uid,
|
|
144
|
+
version: existing.version,
|
|
132
145
|
...(input as Omit<ContactPatch, "uid" | "version">),
|
|
133
146
|
},
|
|
134
147
|
client,
|
|
@@ -151,14 +164,32 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
151
164
|
client,
|
|
152
165
|
);
|
|
153
166
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
setStored({ contact: saved, key });
|
|
168
|
+
const changePhoto = async (current: Contact) => {
|
|
169
|
+
if (pickedPhoto) {
|
|
170
|
+
return uploadContactPhoto(current.uid, current.version, pickedPhoto, client);
|
|
171
|
+
}
|
|
172
|
+
return removePhoto && current.photoBlobKey ? deleteContactPhoto(current.uid, current.version, client) : current;
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
try {
|
|
176
|
+
saved = await changePhoto(saved);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!(err instanceof ApiRequestError && err.status === 409)) {
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
// A version that is stale means an earlier try had been applied after all (its answer never arrived): read the contact as it is and go again, once.
|
|
182
|
+
const current = await getContact(saved.uid, client);
|
|
183
|
+
setStored({ contact: current, key });
|
|
184
|
+
saved = await changePhoto(current);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
// The contact is saved: stay open so the picture can be tried again, or let go of.
|
|
188
|
+
const reason = err instanceof ApiRequestError ? err.message.replace(/\.$/, "") : "the server could not be reached";
|
|
189
|
+
setError(`Saved, but the picture could not be ${pickedPhoto ? "uploaded" : "removed"}: ${reason}.`);
|
|
190
|
+
return;
|
|
158
191
|
}
|
|
159
|
-
|
|
160
|
-
clearPinnedSignerCache();
|
|
161
|
-
onSaved(saved);
|
|
192
|
+
finish(saved);
|
|
162
193
|
} catch (err) {
|
|
163
194
|
setError(err instanceof ApiRequestError ? err.message : "Could not save this contact.");
|
|
164
195
|
} finally {
|
|
@@ -166,6 +197,12 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
166
197
|
}
|
|
167
198
|
}
|
|
168
199
|
|
|
200
|
+
function finish(saved: Contact) {
|
|
201
|
+
// A message opened later in this page load must see the changed contact's signing keys.
|
|
202
|
+
clearPinnedSignerCache();
|
|
203
|
+
onSaved(saved);
|
|
204
|
+
}
|
|
205
|
+
|
|
169
206
|
return (
|
|
170
207
|
// The form fills the pane it is in: the fields scroll in their own area and Save / Cancel stay in view under them (`rr-solid`: opaque over a
|
|
171
208
|
// background image). Outside a bounded pane (the phone's page, which scrolls as a whole) the footer sticks above the bottom tab bar instead.
|
|
@@ -209,7 +246,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
209
246
|
<ContactPhotoField
|
|
210
247
|
displayName={displayName}
|
|
211
248
|
email={emails[0]?.address}
|
|
212
|
-
currentUrl={
|
|
249
|
+
currentUrl={currentPhotoUrl}
|
|
213
250
|
picked={pickedPhoto}
|
|
214
251
|
removed={removePhoto}
|
|
215
252
|
onPick={(file) => {
|
|
@@ -222,6 +259,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
222
259
|
setRemovePhoto(true);
|
|
223
260
|
}}
|
|
224
261
|
onReject={setError}
|
|
262
|
+
onBusyChange={setPreparingPhoto}
|
|
225
263
|
/>
|
|
226
264
|
</div>
|
|
227
265
|
<FavoriteStarButton favorite={favorite} onToggle={() => setFavorite((value) => !value)} />
|
|
@@ -412,10 +450,16 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
412
450
|
</div>
|
|
413
451
|
</div>
|
|
414
452
|
<div className="rr-solid sticky bottom-14 z-10 flex shrink-0 gap-3 border-t border-border bg-surface px-6 py-3 md:bottom-0">
|
|
415
|
-
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
453
|
+
<Button type="submit" loading={saving} disabled={saving || preparingPhoto} className="!w-auto">
|
|
416
454
|
Save
|
|
417
455
|
</Button>
|
|
418
|
-
|
|
456
|
+
{stored && (
|
|
457
|
+
<Button type="button" variant="secondary" className="!w-auto" disabled={saving} onClick={() => finish(stored.contact)}>
|
|
458
|
+
Continue without the picture
|
|
459
|
+
</Button>
|
|
460
|
+
)}
|
|
461
|
+
{/* A contact that is saved already is no longer cancelled: whoever is showing the form needs to hear of it. */}
|
|
462
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={stored ? () => finish(stored.contact) : onCancel}>
|
|
419
463
|
Cancel
|
|
420
464
|
</Button>
|
|
421
465
|
</div>
|
|
@@ -0,0 +1,88 @@
|
|
|
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 ContactPhotoAvatar from "../../../../lib/components/avatar/ContactPhotoAvatar.js";
|
|
7
|
+
import ImageEditBadge from "../../../../lib/components/forms/ImageEditBadge.js";
|
|
8
|
+
import { type Contact, deleteContactPhoto, uploadContactPhoto } from "../../../../lib/contacts/contactsApi.js";
|
|
9
|
+
import { ContactPhotoError, prepareContactPhoto } from "../../../../lib/contacts/preparePhoto.js";
|
|
10
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
11
|
+
import { notify } from "../../notifications/store.js";
|
|
12
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
13
|
+
|
|
14
|
+
export interface ContactPhotoEditorProps {
|
|
15
|
+
/** The stored contact whose picture is changed. */
|
|
16
|
+
contact: Contact;
|
|
17
|
+
displayName: string;
|
|
18
|
+
size: number;
|
|
19
|
+
/** The contact's address, for the Gravatar shown while they have no picture of their own. */
|
|
20
|
+
email?: string;
|
|
21
|
+
/** `"sm"` for an avatar too small for the default badge. */
|
|
22
|
+
badgeSize?: "sm" | "md";
|
|
23
|
+
/** Called with the contact as stored after its picture was set or removed. */
|
|
24
|
+
onChanged: (contact: Contact) => void;
|
|
25
|
+
/** Told whether a change is under way, for what must not edit the contact meanwhile (its star: a second change would send the version this one is about to replace). */
|
|
26
|
+
onBusyChange?: (busy: boolean) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const FAILURE_TITLE = "Couldn't change this contact's photo";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A stored contact's avatar with the camera badge of `ImageEditBadge` at its lower right: unlike the edit form (`ContactPhotoField`), which keeps a chosen picture until the
|
|
33
|
+
* contact is saved, a picture chosen or taken here is made ready and saved at once, and removing it is too. A refusal (a view-only share, a stale version) is a pop-up.
|
|
34
|
+
*/
|
|
35
|
+
export default function ContactPhotoEditor({ contact, displayName, size, email, badgeSize, onChanged, onBusyChange }: ContactPhotoEditorProps) {
|
|
36
|
+
const client = useApiClient();
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
const root = useRef<HTMLDivElement>(null);
|
|
39
|
+
// Preparing a picture takes seconds; what is uploaded against is the contact as it is by then (it may have been read again), not as it was when the picture was chosen.
|
|
40
|
+
const latest = useRef(contact);
|
|
41
|
+
latest.current = contact;
|
|
42
|
+
const wasBusy = useRef(false);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
onBusyChange?.(busy);
|
|
46
|
+
// The badge is disabled meanwhile, which drops its focus: it gets it back, unless the user has gone elsewhere.
|
|
47
|
+
if (wasBusy.current && !busy && (!document.activeElement || document.activeElement === document.body)) {
|
|
48
|
+
root.current?.querySelector<HTMLElement>('button[aria-haspopup="menu"]')?.focus();
|
|
49
|
+
}
|
|
50
|
+
wasBusy.current = busy;
|
|
51
|
+
}, [busy]);
|
|
52
|
+
|
|
53
|
+
async function change(work: () => Promise<Contact>) {
|
|
54
|
+
setBusy(true);
|
|
55
|
+
try {
|
|
56
|
+
onChanged(await work());
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err instanceof ContactPhotoError) {
|
|
59
|
+
notify({ kind: "error", title: FAILURE_TITLE, message: err.message });
|
|
60
|
+
} else {
|
|
61
|
+
notifyApiError(err, FAILURE_TITLE);
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
setBusy(false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div ref={root} className="relative inline-block shrink-0">
|
|
70
|
+
<ContactPhotoAvatar displayName={displayName} size={size} contact={contact} email={email} />
|
|
71
|
+
<ImageEditBadge
|
|
72
|
+
position="bottom-right"
|
|
73
|
+
size={badgeSize}
|
|
74
|
+
label="Change contact photo"
|
|
75
|
+
fileInputLabel="Contact photo file"
|
|
76
|
+
hasImage={!!contact.photoBlobKey}
|
|
77
|
+
busy={busy}
|
|
78
|
+
onFile={(file) =>
|
|
79
|
+
void change(async () => {
|
|
80
|
+
const prepared = await prepareContactPhoto(file);
|
|
81
|
+
return uploadContactPhoto(latest.current.uid, latest.current.version, prepared, client);
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
onRemove={() => void change(() => deleteContactPhoto(latest.current.uid, latest.current.version, client))}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|