@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
|
@@ -45,6 +45,7 @@ export declare const CONTEXT_GRACE_MS = 4000;
|
|
|
45
45
|
export declare function registerOutboxCounter(tracker: (mailboxUid: string) => OutboxCountTracker, onQueued: (mailboxUid: string) => void): () => void;
|
|
46
46
|
/** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server reports later. */
|
|
47
47
|
export declare function retainedRequest(draftUid: string): SendRequest | undefined;
|
|
48
|
+
/** Drops the request a message was queued from, and the pictures remembered as attached to its draft: it has gone, so neither is wanted again. */
|
|
48
49
|
export declare function forgetRetainedRequest(draftUid: string): void;
|
|
49
50
|
/** The sender's signing/encryption key state right now, read from the tab's key session (they can lock mid-send). */
|
|
50
51
|
export declare function readSendKeys(mailboxUid: string, signEnabled: boolean, mailbox: Mailbox | undefined): {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
6
|
-
import { assembleDraft, assembleDraftRaw, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "../../../../lib/mail/mailApi.js";
|
|
6
|
+
import { assembleDraft, assembleDraftRaw, forgetDraftImages, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "../../../../lib/mail/mailApi.js";
|
|
7
7
|
import { describeSendFailure } from "../../../../lib/mail/sendFailure.js";
|
|
8
8
|
import { resolveRecipientEncryption } from "../../../../lib/crypto/composeSecurity.js";
|
|
9
9
|
import { getUnlockedKeys } from "../../../../lib/crypto/keySession.js";
|
|
@@ -33,8 +33,10 @@ const RETAINED_LIMIT = 25;
|
|
|
33
33
|
export function retainedRequest(draftUid) {
|
|
34
34
|
return sendState.retained.get(draftUid);
|
|
35
35
|
}
|
|
36
|
+
/** Drops the request a message was queued from, and the pictures remembered as attached to its draft: it has gone, so neither is wanted again. */
|
|
36
37
|
export function forgetRetainedRequest(draftUid) {
|
|
37
38
|
sendState.retained.delete(draftUid);
|
|
39
|
+
forgetDraftImages(draftUid);
|
|
38
40
|
}
|
|
39
41
|
function retain(request) {
|
|
40
42
|
sendState.retained.delete(request.draft.uid);
|
|
@@ -310,9 +312,11 @@ export function startSend(request, client) {
|
|
|
310
312
|
else if (outcome.kind === "sent") {
|
|
311
313
|
// A server with no queue relayed it before answering: the Sent Items copy exists already.
|
|
312
314
|
markOutgoingSent(uid);
|
|
315
|
+
forgetDraftImages(uid);
|
|
313
316
|
notifySent();
|
|
314
317
|
}
|
|
315
318
|
else {
|
|
319
|
+
forgetDraftImages(uid);
|
|
316
320
|
notify({
|
|
317
321
|
id: `scheduled:${uid}`,
|
|
318
322
|
kind: "success",
|
|
@@ -8,6 +8,9 @@ import { FolderChange } from "./folderTree.js";
|
|
|
8
8
|
export declare const LIVE_POLL_INTERVAL_MS = 45000;
|
|
9
9
|
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
10
10
|
export declare const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
11
|
+
/** Refreshes are also at least this far apart: while mail keeps arriving (a bulk delivery, an import) the list, the folder counts and an open
|
|
12
|
+
* thread are read about once in this time - a trailing refresh then picks up whatever came since - instead of every debounce window. */
|
|
13
|
+
export declare const LIVE_EVENT_MIN_INTERVAL_MS = 1500;
|
|
11
14
|
/** What changed, as far as a list on screen is concerned. */
|
|
12
15
|
export interface LiveUpdates {
|
|
13
16
|
/** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
|
|
@@ -13,6 +13,9 @@ import { sortFolders } from "./folderTree.js";
|
|
|
13
13
|
export const LIVE_POLL_INTERVAL_MS = 45000;
|
|
14
14
|
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
15
15
|
export const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
16
|
+
/** Refreshes are also at least this far apart: while mail keeps arriving (a bulk delivery, an import) the list, the folder counts and an open
|
|
17
|
+
* thread are read about once in this time - a trailing refresh then picks up whatever came since - instead of every debounce window. */
|
|
18
|
+
export const LIVE_EVENT_MIN_INTERVAL_MS = 1500;
|
|
16
19
|
export const NO_LIVE_UPDATES = { tick: 0, folderUids: null };
|
|
17
20
|
/** The class names `NotificationUtils.sendMessage()` publishes are the model's (`MessageMongo`, `MessageSQL`, `FolderMongo`). */
|
|
18
21
|
const MESSAGE_EVENT = /^Message/;
|
|
@@ -88,9 +91,11 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
|
|
|
88
91
|
let pendingFolders = new Set();
|
|
89
92
|
let pendingUnknown = false;
|
|
90
93
|
let everOpen = false;
|
|
94
|
+
let lastFlushAt = -Infinity;
|
|
91
95
|
// Never runs once `stopped`: whatever sets it clears the timer first.
|
|
92
96
|
function flush() {
|
|
93
97
|
timer = undefined;
|
|
98
|
+
lastFlushAt = Date.now();
|
|
94
99
|
const folderUids = pendingUnknown ? null : new Set(pendingFolders);
|
|
95
100
|
pendingFolders = new Set();
|
|
96
101
|
pendingUnknown = false;
|
|
@@ -108,7 +113,7 @@ export function useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolde
|
|
|
108
113
|
else {
|
|
109
114
|
pendingUnknown = true;
|
|
110
115
|
}
|
|
111
|
-
timer ?? (timer = setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS));
|
|
116
|
+
timer ?? (timer = setTimeout(flush, Math.max(LIVE_EVENT_DEBOUNCE_MS, lastFlushAt + LIVE_EVENT_MIN_INTERVAL_MS - Date.now())));
|
|
112
117
|
}
|
|
113
118
|
const offEvent = client.onEvent((event) => {
|
|
114
119
|
const sendEvent = parseSendEvent(event);
|
|
@@ -7,6 +7,8 @@ export interface EmptiableFolder {
|
|
|
7
7
|
name: string;
|
|
8
8
|
/** Every folder to empty, when "the folder" is the "All mailboxes" view of one type (each mailbox's own Deleted Items): `uid` is then the first of them. */
|
|
9
9
|
uids?: string[];
|
|
10
|
+
/** The names of the mailboxes those folders are in, for the confirmation to say whose are emptied (in the "All mailboxes" view). */
|
|
11
|
+
scope?: string[];
|
|
10
12
|
}
|
|
11
13
|
export interface PermanentDelete {
|
|
12
14
|
/**
|
|
@@ -97,7 +97,7 @@ export function usePermanentDelete() {
|
|
|
97
97
|
? "all items"
|
|
98
98
|
: folderRequest.count === 1
|
|
99
99
|
? "the only item"
|
|
100
|
-
: `all ${folderRequest.count} items`} in ${folderRequest.folder.name}? This can't be undone.`
|
|
100
|
+
: `all ${folderRequest.count} items`} in ${folderRequest.folder.name}${folderRequest.folder.scope ? ` of: ${folderRequest.folder.scope.join(", ")}` : ""}? This can't be undone.`
|
|
101
101
|
: `Permanently delete ${messageCount(request?.kind === "messages" ? request.messages.length : 0)}? This can't be undone.`, confirmLabel: folderRequest ? "Delete all permanently" : "Delete permanently", busy: running, onConfirm: () => void confirm(), onCancel: cancel }));
|
|
102
102
|
return { requestPermanentDelete, requestEmptyFolder, busy: pending !== null, dialog };
|
|
103
103
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { logSessionEvent } from "../../../lib/auth/session.js";
|
|
5
6
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
6
7
|
import { describeSendFailure } from "../../../lib/mail/sendFailure.js";
|
|
7
8
|
import { notify } from "./store.js";
|
|
@@ -16,6 +17,7 @@ export function setSignInUrl(authServerUrl) {
|
|
|
16
17
|
}
|
|
17
18
|
/** The "Your session expired" pop-up: one however many requests noticed, with a "Sign in" action. Returns its id. */
|
|
18
19
|
export function notifySessionExpired() {
|
|
20
|
+
logSessionEvent("notice:session-expired");
|
|
19
21
|
return notify({
|
|
20
22
|
kind: "error",
|
|
21
23
|
title: "Your session expired",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The user's one switch for every pop-up (new mail, a failed send, an API error, a reminder), per browser in `localStorage`.
|
|
3
3
|
* Framework-free like the store, which reads it: `notify()` keeps a notification out of sight while it is off (it is still
|
|
4
|
-
* listed in "Recent notifications").
|
|
4
|
+
* listed in "Recent notifications") - except an error that carries actions (Retry, Open draft), which would otherwise be unreachable.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* `localStorage` key of the switch. Absent means on; `"off"` means the user turned pop-ups off. The name is from when the switch
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* The user's one switch for every pop-up (new mail, a failed send, an API error, a reminder), per browser in `localStorage`.
|
|
7
7
|
* Framework-free like the store, which reads it: `notify()` keeps a notification out of sight while it is off (it is still
|
|
8
|
-
* listed in "Recent notifications").
|
|
8
|
+
* listed in "Recent notifications") - except an error that carries actions (Retry, Open draft), which would otherwise be unreachable.
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* `localStorage` key of the switch. Absent means on; `"off"` means the user turned pop-ups off. The name is from when the switch
|
|
@@ -232,7 +232,9 @@ export function notify(input) {
|
|
|
232
232
|
paused: false,
|
|
233
233
|
};
|
|
234
234
|
applyInput(entry, input);
|
|
235
|
-
|
|
235
|
+
// An error that offers something to do (a failed send's Retry and Open draft, "Sign in again") is shown whatever the switch says: it is
|
|
236
|
+
// the only way back to what the person typed, and the history has no way to carry out its actions.
|
|
237
|
+
if (!getNotificationsEnabled() && !(entry.kind === "error" && entry.actions.length > 0)) {
|
|
236
238
|
// The user has pop-ups off: nothing is shown, but it is listed in the history like any other.
|
|
237
239
|
record(entry);
|
|
238
240
|
emit();
|
|
@@ -35,7 +35,8 @@ export interface PluginNavProps {
|
|
|
35
35
|
pluginNav?: PluginNav;
|
|
36
36
|
}
|
|
37
37
|
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
38
|
-
* protocol-relative URLs to another host)
|
|
38
|
+
* protocol-relative URLs to another host), and holds no control character, space or backslash anywhere -
|
|
39
|
+
* URL parsers strip tab/CR/LF from anywhere, so `/<tab>/evil.com` would otherwise read as `//evil.com`. */
|
|
39
40
|
export declare function isSafePluginHref(href: string): boolean;
|
|
40
41
|
/**
|
|
41
42
|
* Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
/** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
|
|
6
|
-
* protocol-relative URLs to another host)
|
|
6
|
+
* protocol-relative URLs to another host), and holds no control character, space or backslash anywhere -
|
|
7
|
+
* URL parsers strip tab/CR/LF from anywhere, so `/<tab>/evil.com` would otherwise read as `//evil.com`. */
|
|
7
8
|
export function isSafePluginHref(href) {
|
|
8
|
-
|
|
9
|
+
// eslint-disable-next-line no-control-regex -- the control characters are exactly what is being rejected
|
|
10
|
+
return /^\/(?![/\\])[^\u0000-\u0020\u007f\\]*$/.test(href);
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
9
9
|
import { HiOutlineBars3, HiOutlinePlus } from "react-icons/hi2";
|
|
10
10
|
import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, parseISO, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
|
|
@@ -17,6 +17,7 @@ import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calen
|
|
|
17
17
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
18
18
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
19
19
|
import { expandAllOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
20
|
+
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
20
21
|
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
21
22
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
22
23
|
import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
@@ -46,7 +47,7 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
46
47
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
47
48
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
48
49
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
49
|
-
import {
|
|
50
|
+
import { changedEventOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
50
51
|
/** The furthest instants a `Date` can hold: the List's "range" reaches both. */
|
|
51
52
|
const MAX_DATE_MS = 8.64e15;
|
|
52
53
|
function CalendarPage(props) {
|
|
@@ -236,15 +237,20 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
236
237
|
const calendarFolderUidsKey = calendarFolders.map((f) => f.uid).sort().join(",");
|
|
237
238
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
238
239
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
239
|
-
//
|
|
240
|
-
// see (see `
|
|
241
|
-
// reader may see - all of it for the owner - and replaces (or joins) what the calendar holds; one that is gone (a 404) leaves it. A failed
|
|
242
|
-
// changes nothing: the next load shows the event as it is.
|
|
240
|
+
// An event that somebody creates, changes or deletes is announced on the push connection - a private or confidential one as its busy block, whatever the
|
|
241
|
+
// reader may see (see `changedEventOf()`): that payload is never stored here. The event is fetched again by its uid, which comes back as much as this
|
|
242
|
+
// reader may see - all of it for the owner - and replaces (or joins) what the calendar holds; one that is gone (a 404, or a delete) leaves it. A failed
|
|
243
|
+
// fetch changes nothing: the next load shows the event as it is.
|
|
243
244
|
const checkedFolderUidsRef = useRef(checkedFolderUids);
|
|
244
245
|
checkedFolderUidsRef.current = checkedFolderUids;
|
|
245
246
|
useEffect(() => getPushClient().onEvent((event) => {
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
247
|
+
const change = changedEventOf(event);
|
|
248
|
+
if (!change) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const { uid } = change;
|
|
252
|
+
if (change.deleted) {
|
|
253
|
+
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
248
254
|
return;
|
|
249
255
|
}
|
|
250
256
|
void getCalendarEvent(uid, client).then((fresh) => {
|
|
@@ -272,11 +278,14 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
272
278
|
return () => clearTimeout(timer);
|
|
273
279
|
}, [searchText]);
|
|
274
280
|
// The List shows only what is from today on, so that is all a search steps through there; the other views reach the past ones.
|
|
281
|
+
// Today moves on at midnight: the List's "from today on" is worked out again then (`listDay` is only a dependency).
|
|
282
|
+
const listDay = useDayKey();
|
|
283
|
+
const dayOfList = view === "list" ? listDay : "";
|
|
275
284
|
const matches = useMemo(() => {
|
|
276
285
|
const found = searching ? searchOccurrences(events, query) : [];
|
|
277
286
|
const today = new Date();
|
|
278
287
|
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
279
|
-
}, [events, query, searching, view]);
|
|
288
|
+
}, [events, query, searching, view, dayOfList]);
|
|
280
289
|
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
281
290
|
// What an open event card steps through, in order: the matches while a search runs, else every event of the checked calendars (bounded as a
|
|
282
291
|
// search is). The List holds only what is from today on, so that is all its card steps through. Worked out only when something needs it.
|
|
@@ -290,7 +299,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
290
299
|
return everything.filter((occurrence) => isUpcoming(occurrence, today));
|
|
291
300
|
}
|
|
292
301
|
return everything;
|
|
293
|
-
}, [searching, matches, view, everything]);
|
|
302
|
+
}, [searching, matches, view, everything, dayOfList]);
|
|
294
303
|
const occurrences = useMemo(() => {
|
|
295
304
|
if (view === "list") {
|
|
296
305
|
return sequence;
|
|
@@ -425,11 +434,12 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
425
434
|
initialAllDay,
|
|
426
435
|
});
|
|
427
436
|
}
|
|
428
|
-
|
|
437
|
+
// Stable (it only sets state), so the List's rows are not rendered again by every keystroke in the search box.
|
|
438
|
+
const openEvent = useCallback((occurrence) => {
|
|
429
439
|
setSlide(undefined);
|
|
430
440
|
setActiveKey(occurrence.occurrenceKey);
|
|
431
441
|
setModal({ occurrence });
|
|
432
|
-
}
|
|
442
|
+
}, []);
|
|
433
443
|
function closeModal() {
|
|
434
444
|
setModal(null);
|
|
435
445
|
}
|
|
@@ -477,6 +487,8 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
477
487
|
}
|
|
478
488
|
catch (err) {
|
|
479
489
|
notifyApiError(err, action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event");
|
|
490
|
+
// A refused change (a 409 most of all) means the page holds a stale version: load again, or every later drag of this event fails the same way.
|
|
491
|
+
reload();
|
|
480
492
|
}
|
|
481
493
|
}
|
|
482
494
|
const title = view === "list"
|
|
@@ -9,12 +9,13 @@ import { useEffect, useState } from "react";
|
|
|
9
9
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
10
10
|
import { deleteContact, getContact } from "../../../lib/contacts/contactsApi.js";
|
|
11
11
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
12
|
-
import ContactsShell from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
12
|
+
import ContactsShell, { useContactsShell } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
13
13
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
14
14
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
15
15
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
16
16
|
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
17
17
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
18
|
+
import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
|
|
18
19
|
/**
|
|
19
20
|
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
20
21
|
* existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
|
|
@@ -31,6 +32,9 @@ function ContactDetailContent({ uid }) {
|
|
|
31
32
|
const [loading, setLoading] = useState(true);
|
|
32
33
|
const [error, setError] = useState(null);
|
|
33
34
|
const [mode, setMode] = useState("view");
|
|
35
|
+
const { mailboxes } = useContactsShell();
|
|
36
|
+
// A contact in a mailbox shared with the reader view-only can be read, not changed.
|
|
37
|
+
const writable = useMailboxUpdateAccess(mailboxes.find((mailbox) => mailbox.uid === contact?.mailboxUid));
|
|
34
38
|
useEffect(() => {
|
|
35
39
|
setLoading(true);
|
|
36
40
|
setError(null);
|
|
@@ -72,7 +76,7 @@ function ContactDetailContent({ uid }) {
|
|
|
72
76
|
setMode("view");
|
|
73
77
|
}, onCancel: () => setMode("view") })] }));
|
|
74
78
|
}
|
|
75
|
-
return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref, onKeysChanged: handleKeysChanged, onChanged: setContact }) }));
|
|
79
|
+
return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), canWrite: writable, backHref: backHref, onKeysChanged: handleKeysChanged, onChanged: setContact }) }));
|
|
76
80
|
}
|
|
77
81
|
export default ContactDetailPage;
|
|
78
82
|
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
|
@@ -7,8 +7,9 @@ import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
|
7
7
|
import { useNavigate } from "../../shared/navigation/index.js";
|
|
8
8
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
10
|
-
import { createContact, deleteContact, listContacts, listDeletedContacts, setContactFavorite,
|
|
11
|
-
import { contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
10
|
+
import { createContact, deleteContact, listContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
|
|
11
|
+
import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
12
|
+
import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
|
|
12
13
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
13
14
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
14
15
|
import { useCompose } from "../../shared/components/mail/compose/ComposeContext.js";
|
|
@@ -17,11 +18,12 @@ import ContactsSidebar from "../../shared/components/contacts/ContactsSidebar.js
|
|
|
17
18
|
import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js";
|
|
18
19
|
import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
|
|
19
20
|
import { HiOutlineUserPlus } from "react-icons/hi2";
|
|
20
|
-
import
|
|
21
|
+
import ContactPhotoAvatar from "../../../lib/components/avatar/ContactPhotoAvatar.js";
|
|
21
22
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
22
23
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
23
24
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
24
25
|
import { getMyMailboxAccess } from "../../../lib/mail/mailboxAccessApi.js";
|
|
26
|
+
import { useMailboxUpdateAccess } from "../../shared/mail/useMailboxUpdateAccess.js";
|
|
25
27
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
26
28
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
27
29
|
import Modal from "../../../lib/components/overlays/Modal.js";
|
|
@@ -30,6 +32,7 @@ import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigne
|
|
|
30
32
|
import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
31
33
|
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
32
34
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
35
|
+
import { notify } from "../../shared/notifications/store.js";
|
|
33
36
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
34
37
|
function ContactsPage(props) {
|
|
35
38
|
return (_jsx(ContactsShell, { ...props, children: _jsx(ContactsContent, { userUid: props.userUid }) }));
|
|
@@ -57,6 +60,8 @@ function ContactsContent({ userUid }) {
|
|
|
57
60
|
const { folderUid, mailboxUid, mailboxes } = useContactsShell();
|
|
58
61
|
// The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
|
|
59
62
|
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
|
|
63
|
+
// A mailbox shared with the reader view-only: its contacts can be read, not changed.
|
|
64
|
+
const mailboxWritable = useMailboxUpdateAccess(mailboxes.find((mb) => mb.uid === mailboxUid));
|
|
60
65
|
const { openCompose } = useCompose();
|
|
61
66
|
const isMobile = useIsMobile();
|
|
62
67
|
const navigate = useNavigate();
|
|
@@ -263,6 +268,22 @@ function ContactsContent({ userUid }) {
|
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
// The bulk handlers below try every contact and report each failure: the same failure repeated is one pop-up with a count.
|
|
271
|
+
// A bulk action sends every checked row's own version: pressed again before it finishes, it would send stale ones and raise a stream of 409s.
|
|
272
|
+
const bulkRunning = useRef(false);
|
|
273
|
+
function singleFlight(run) {
|
|
274
|
+
return async () => {
|
|
275
|
+
if (bulkRunning.current) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
bulkRunning.current = true;
|
|
279
|
+
try {
|
|
280
|
+
await run();
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
bulkRunning.current = false;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
266
287
|
async function handleBulkDelete() {
|
|
267
288
|
setConfirmingBulkDelete(false);
|
|
268
289
|
for (const contact of checkedContacts) {
|
|
@@ -291,7 +312,7 @@ function ContactsContent({ userUid }) {
|
|
|
291
312
|
// contacts happen to be open - the compose window's From field can still change it.
|
|
292
313
|
openCompose({ to: addresses.join(", ") });
|
|
293
314
|
}
|
|
294
|
-
|
|
315
|
+
const handleToggleFavorite = singleFlight(async () => {
|
|
295
316
|
const allFavorited = checkedContacts.every((c) => c.favorite);
|
|
296
317
|
for (const contact of checkedContacts) {
|
|
297
318
|
try {
|
|
@@ -303,8 +324,8 @@ function ContactsContent({ userUid }) {
|
|
|
303
324
|
}
|
|
304
325
|
clearPinnedSignerCache();
|
|
305
326
|
await reload();
|
|
306
|
-
}
|
|
307
|
-
|
|
327
|
+
});
|
|
328
|
+
const handleAddCategory = singleFlight(async () => {
|
|
308
329
|
const category = window.prompt("Category name");
|
|
309
330
|
if (!category?.trim()) {
|
|
310
331
|
return;
|
|
@@ -322,7 +343,7 @@ function ContactsContent({ userUid }) {
|
|
|
322
343
|
}
|
|
323
344
|
clearPinnedSignerCache();
|
|
324
345
|
await reload();
|
|
325
|
-
}
|
|
346
|
+
});
|
|
326
347
|
function handleExportVCard() {
|
|
327
348
|
downloadTextFile(checkedContacts.length === 1 ? `${checkedContacts[0].displayName}.vcf` : "contacts.vcf", checkedContacts.length === 1 ? contactToVCard(checkedContacts[0]) : contactsToVCardFile(checkedContacts));
|
|
328
349
|
}
|
|
@@ -330,9 +351,37 @@ function ContactsContent({ userUid }) {
|
|
|
330
351
|
if (!mailboxUid || !folderUid) {
|
|
331
352
|
return;
|
|
332
353
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
354
|
+
// A file is only read, and only so many records taken from it (see `parseVCards()`): one contact is one request, and a huge export would
|
|
355
|
+
// lock the page for minutes and flood the server. Whatever goes wrong while reading it is said, not left as an unhandled rejection.
|
|
356
|
+
if (file.size > VCARD_MAX_CHARS) {
|
|
357
|
+
notify({ kind: "error", title: "Couldn't import contacts", message: `That file is larger than ${Math.round(VCARD_MAX_CHARS / 1000000)} MB. Split it into smaller files.` });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
let parsed;
|
|
361
|
+
try {
|
|
362
|
+
parsed = parseVCards(await file.text());
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
notifyApiError(err, "Couldn't read that contacts file");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (parsed.length >= VCARD_MAX_CONTACTS) {
|
|
369
|
+
notify({ kind: "warning", title: "Only part of the file was imported", message: `A file is imported up to its first ${VCARD_MAX_CONTACTS} contacts - import the rest from another file.` });
|
|
370
|
+
}
|
|
371
|
+
// Importing a file twice must not double every contact: one whose address (or, with no address, name and phone) is already a contact's - or an
|
|
372
|
+
// earlier card's in the file - is left alone. The folder is read in full for that, not just the page of it the list holds.
|
|
373
|
+
let fresh;
|
|
374
|
+
try {
|
|
375
|
+
fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
notifyApiError(err, "Couldn't check the file against your contacts");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (fresh.length < parsed.length) {
|
|
382
|
+
notify({ kind: "info", title: "Some contacts were skipped", message: `${parsed.length - fresh.length} of the contacts in that file are already in your contacts.` });
|
|
383
|
+
}
|
|
384
|
+
for (const input of fresh) {
|
|
336
385
|
try {
|
|
337
386
|
await createContact({ mailboxUid, folderUid, ...input }, client);
|
|
338
387
|
}
|
|
@@ -346,7 +395,9 @@ function ContactsContent({ userUid }) {
|
|
|
346
395
|
return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row md:flex-none md:h-[calc(100dvh_-_var(--rr-header-h,4rem))] md:overflow-hidden", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted }), _jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex", children: [_jsxs("div", { className: [
|
|
347
396
|
"w-full md:w-[clamp(16rem,32vw,26rem)] min-w-0 md:shrink-0 md:border-r border-border md:flex flex-col min-h-0",
|
|
348
397
|
mode === "new" ? "hidden md:flex" : "flex",
|
|
349
|
-
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
|
|
398
|
+
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button",
|
|
399
|
+
// A deleted contact has no pane to show (it can't be restored or opened), and its route would answer 404.
|
|
400
|
+
disabled: isDeletedView, onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left disabled:cursor-default", children: [_jsx(ContactPhotoAvatar, { lazy: true, displayName: contact.displayName, size: 28, contact: contact, email: contact.emails[0]?.address }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), canWrite: mailboxWritable, onKeysChanged: () => void reload(), onChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
|
|
350
401
|
}
|
|
351
402
|
export default ContactsPage;
|
|
352
403
|
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|