@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
package/dist/lib/auth/session.js
CHANGED
|
@@ -23,9 +23,31 @@ export const SESSION_REFRESH_AFTER_MS = 45 * 60 * 1000;
|
|
|
23
23
|
export const SESSION_REFRESH_CHECK_MS = 60 * 1000;
|
|
24
24
|
/** A reload that follows a successful recovery refresh but still finds no session is not repeated within this window. */
|
|
25
25
|
export const SESSION_RELOAD_GUARD_MS = 60 * 1000;
|
|
26
|
+
/** A refresh answered with a refusal is asked once more after this long, in case the cookies were rotated by another tab or the server's session save lagged. */
|
|
27
|
+
export const SESSION_REFRESH_RETRY_MS = 1500;
|
|
28
|
+
/** A forced refresh (a request was refused, or the page has no session) does nothing when a refresh succeeded within this long: the refusal came from the old cookies. */
|
|
29
|
+
export const SESSION_REFRESH_MIN_GAP_MS = 10 * 1000;
|
|
30
|
+
/** How many entries `rapidmx.session.log` keeps. */
|
|
31
|
+
export const SESSION_LOG_MAX = 40;
|
|
26
32
|
const LOCK_NAME = "rapidmx-session-refresh";
|
|
27
33
|
const REFRESHED_AT_KEY = "rapidmx.session.refreshedAt";
|
|
28
34
|
const RELOADED_AT_KEY = "rapidmx.session.reloadedAt";
|
|
35
|
+
const LOG_KEY = "rapidmx.session.log";
|
|
36
|
+
/**
|
|
37
|
+
* How long a refresh request may go unanswered before it is given up on (not aborted - see `postRefresh()`). A refresh runs inside a cross-tab Web Lock, so one that hangs would hold the lock - and
|
|
38
|
+
* with it every tab's refresh, and every request waiting on a recovery - for good.
|
|
39
|
+
*/
|
|
40
|
+
export const SESSION_REFRESH_TIMEOUT_MS = 15 * 1000;
|
|
41
|
+
let retryDelayMs = SESSION_REFRESH_RETRY_MS;
|
|
42
|
+
let refreshTimeoutMs = SESSION_REFRESH_TIMEOUT_MS;
|
|
43
|
+
/** Sets how long a refresh request may go unanswered (for tests); `resetSessionRecovery()` restores the default. */
|
|
44
|
+
export function setSessionRefreshTimeout(ms) {
|
|
45
|
+
refreshTimeoutMs = ms;
|
|
46
|
+
}
|
|
47
|
+
/** Sets how long a refused refresh waits before its one retry (for tests); `resetSessionRecovery()` restores the default. */
|
|
48
|
+
export function setSessionRefreshRetryDelay(ms) {
|
|
49
|
+
retryDelayMs = ms;
|
|
50
|
+
}
|
|
29
51
|
function readTimestamp(storage, key) {
|
|
30
52
|
try {
|
|
31
53
|
const value = Number(storage().getItem(key));
|
|
@@ -43,42 +65,175 @@ function writeTimestamp(storage, key, value) {
|
|
|
43
65
|
// Storage can be blocked or full; the timestamp only avoids redundant refreshes.
|
|
44
66
|
}
|
|
45
67
|
}
|
|
68
|
+
/** Whether a refresh succeeded within the last `ms`, in this tab or another (they share `localStorage`). A cleared or future-dated record counts as no. */
|
|
69
|
+
export function refreshedRecently(ms) {
|
|
70
|
+
const at = readTimestamp(() => localStorage, REFRESHED_AT_KEY);
|
|
71
|
+
const age = Date.now() - at;
|
|
72
|
+
return at > 0 && age >= 0 && age < ms;
|
|
73
|
+
}
|
|
74
|
+
/** The session log: the last `SESSION_LOG_MAX` things the silent refresh did, oldest first, kept in `localStorage['rapidmx.session.log']` so the cause of an unexpected sign-in can be read afterward. */
|
|
75
|
+
export function getSessionLog() {
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(localStorage.getItem(LOG_KEY) ?? "[]");
|
|
78
|
+
return Array.isArray(value) ? value : [];
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Appends to the session log, dropping the oldest entries past `SESSION_LOG_MAX`. Best effort: blocked storage loses the entry. */
|
|
85
|
+
export function logSessionEvent(e, d) {
|
|
86
|
+
try {
|
|
87
|
+
const entries = getSessionLog();
|
|
88
|
+
entries.push(d === undefined ? { t: new Date().toISOString(), e } : { t: new Date().toISOString(), e, d });
|
|
89
|
+
localStorage.setItem(LOG_KEY, JSON.stringify(entries.slice(-SESSION_LOG_MAX)));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// The log is evidence only; it must never get in the way of the session.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** How long ago the last successful refresh was, for log entries: `12s`, or `never` when this browser has no record. */
|
|
96
|
+
function sinceLastRefresh() {
|
|
97
|
+
const at = readTimestamp(() => localStorage, REFRESHED_AT_KEY);
|
|
98
|
+
return at > 0 ? `${Math.round((Date.now() - at) / 1000)}s` : "never";
|
|
99
|
+
}
|
|
46
100
|
/** The URL of auth-server's sign-in page, carrying the current URL as `return_to` so it can send the browser back here. */
|
|
47
101
|
function signInUrl(authServerUrl) {
|
|
48
102
|
return `${authServerUrl}/auth/signin?return_to=${encodeURIComponent(window.location.href)}`;
|
|
49
103
|
}
|
|
104
|
+
/** Sends the browser to auth-server's sign-in page, first writing why to the session log and printing the log to the console. */
|
|
105
|
+
function goToSignIn(authServerUrl, reason) {
|
|
106
|
+
logSessionEvent("leave", `${reason} sinceOk=${sinceLastRefresh()}`);
|
|
107
|
+
console.info("[rapidmx] Going to sign-in. Session log:", getSessionLog());
|
|
108
|
+
window.location.href = signInUrl(authServerUrl);
|
|
109
|
+
}
|
|
110
|
+
/** Whether `err` says auth-server rejected the refresh token itself, as opposed to a failure worth retrying. */
|
|
111
|
+
function isAuthRejection(err) {
|
|
112
|
+
return err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
|
|
113
|
+
}
|
|
114
|
+
function describeFailure(err) {
|
|
115
|
+
if (err instanceof ApiRequestError) {
|
|
116
|
+
return `status=${err.status}${err.code ? ` code=${err.code}` : ""} sinceOk=${sinceLastRefresh()}`;
|
|
117
|
+
}
|
|
118
|
+
return `${err instanceof Error ? err.message : "unknown"}`.slice(0, 80);
|
|
119
|
+
}
|
|
120
|
+
/** Counts the refresh requests made, so a request that answers after it was given up on can tell whether a newer one has started since. */
|
|
121
|
+
let refreshSeq = 0;
|
|
122
|
+
/**
|
|
123
|
+
* One `POST /auth/refresh`, recording when it succeeded. Given up on after `SESSION_REFRESH_TIMEOUT_MS`, which is a failure worth retrying, not a rejection.
|
|
124
|
+
* Giving up only stops waiting: the request is NOT aborted. Auth-server rotates the single-use refresh token as it answers, and a browser that aborts before the
|
|
125
|
+
* response headers arrive never stores the rotated cookie, leaving the consumed token behind - the next refresh would be refused and the user signed out.
|
|
126
|
+
* A late answer is still logged, and a late success still recorded unless a newer refresh has started since.
|
|
127
|
+
*/
|
|
128
|
+
async function postRefresh(authServerUrl) {
|
|
129
|
+
const seq = ++refreshSeq;
|
|
130
|
+
const request = authApiFetch(authServerUrl, "/auth/refresh", { method: "POST" });
|
|
131
|
+
let timedOut = false;
|
|
132
|
+
let timer;
|
|
133
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
134
|
+
timer = setTimeout(() => {
|
|
135
|
+
timedOut = true;
|
|
136
|
+
reject(new Error(`refresh timed out after ${refreshTimeoutMs}ms`));
|
|
137
|
+
}, refreshTimeoutMs);
|
|
138
|
+
});
|
|
139
|
+
// Handles the request's own outcome whenever it comes, so a rejection after the timeout is never unhandled.
|
|
140
|
+
request.then(() => {
|
|
141
|
+
if (!timedOut) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (seq === refreshSeq) {
|
|
145
|
+
writeTimestamp(() => localStorage, REFRESHED_AT_KEY, Date.now());
|
|
146
|
+
logSessionEvent("refresh:late-ok");
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
logSessionEvent("refresh:late-superseded");
|
|
150
|
+
}
|
|
151
|
+
}, (err) => {
|
|
152
|
+
if (timedOut) {
|
|
153
|
+
logSessionEvent("refresh:late-error", describeFailure(err));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
await Promise.race([request, timeout]);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
}
|
|
162
|
+
writeTimestamp(() => localStorage, REFRESHED_AT_KEY, Date.now());
|
|
163
|
+
}
|
|
164
|
+
let refreshing;
|
|
50
165
|
/**
|
|
51
166
|
* Refreshes the signed-in session through auth-server: exchanges the `refresh` cookie for a new access token and
|
|
52
167
|
* refresh token, both set as cookies on the response. Rejects with the `ApiRequestError` auth-server answered
|
|
53
168
|
* (`401`/`403` when the refresh token has expired or was revoked).
|
|
54
169
|
*
|
|
55
|
-
* A refresh token is single-use and bound to the session,
|
|
56
|
-
*
|
|
57
|
-
*
|
|
170
|
+
* A refresh token is single-use and bound to the session, and auth-server saves the rotated token a moment after it
|
|
171
|
+
* answers, so refreshing twice in quick succession makes the second look like a dead session. Refreshes are therefore
|
|
172
|
+
* shared by every caller in the tab (one in flight at a time), serialised across tabs (Web Locks, where the browser has
|
|
173
|
+
* them), and each tab records when the last one succeeded, so a caller that gets its turn right after a refresh does
|
|
174
|
+
* nothing. A refresh that is refused is asked once more after `SESSION_REFRESH_RETRY_MS` (the cookies may have been
|
|
175
|
+
* rotated meanwhile) before it counts as a rejection.
|
|
58
176
|
*
|
|
59
|
-
* @param force Refresh even when the last refresh, by this tab or another, was
|
|
177
|
+
* @param force Refresh even when the last refresh, by this tab or another, was `SESSION_REFRESH_AFTER_MS` ago or less. Even forced, a refresh made within `SESSION_REFRESH_MIN_GAP_MS` is not repeated.
|
|
178
|
+
* @param reason Why, for the session log.
|
|
60
179
|
* @returns `true` when a refresh was made, `false` when one was not needed.
|
|
61
180
|
*/
|
|
62
|
-
export
|
|
181
|
+
export function refreshSession(authServerUrl, force = false, reason = force ? "recovery" : "check") {
|
|
182
|
+
if (refreshing) {
|
|
183
|
+
// A forced caller must not take the outcome of a refresh that may have been skipped as not due: ask again once it is over (which the minimum gap keeps from being a second POST when it did refresh).
|
|
184
|
+
return force && !refreshing.forced ? refreshing.promise.then(() => refreshSession(authServerUrl, true, reason)) : refreshing.promise;
|
|
185
|
+
}
|
|
63
186
|
const run = async () => {
|
|
64
|
-
if (
|
|
187
|
+
if (refreshedRecently(force ? SESSION_REFRESH_MIN_GAP_MS : SESSION_REFRESH_AFTER_MS)) {
|
|
188
|
+
if (force) {
|
|
189
|
+
logSessionEvent("refresh:skip", `recent sinceOk=${sinceLastRefresh()} reason=${reason}`);
|
|
190
|
+
}
|
|
65
191
|
return false;
|
|
66
192
|
}
|
|
67
|
-
|
|
68
|
-
|
|
193
|
+
logSessionEvent("refresh:start", `reason=${reason} sinceOk=${sinceLastRefresh()}`);
|
|
194
|
+
try {
|
|
195
|
+
await postRefresh(authServerUrl);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (!isAuthRejection(err)) {
|
|
199
|
+
logSessionEvent("refresh:error", describeFailure(err));
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
logSessionEvent("refresh:rejected", `${describeFailure(err)} attempt=1`);
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
204
|
+
if (refreshedRecently(SESSION_REFRESH_MIN_GAP_MS)) {
|
|
205
|
+
// Another tab renewed the session while this one waited.
|
|
206
|
+
logSessionEvent("refresh:skip", `recent-after-retry-wait reason=${reason}`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
logSessionEvent("refresh:retry", `reason=${reason}`);
|
|
210
|
+
try {
|
|
211
|
+
await postRefresh(authServerUrl);
|
|
212
|
+
}
|
|
213
|
+
catch (err2) {
|
|
214
|
+
logSessionEvent(isAuthRejection(err2) ? "refresh:rejected" : "refresh:error", `${describeFailure(err2)} attempt=2`);
|
|
215
|
+
throw err2;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
logSessionEvent("refresh:ok", `reason=${reason}`);
|
|
69
219
|
return true;
|
|
70
220
|
};
|
|
71
221
|
const locks = typeof navigator === "undefined" ? undefined : navigator.locks;
|
|
72
|
-
|
|
222
|
+
const promise = (locks ? locks.request(LOCK_NAME, run) : run()).finally(() => {
|
|
223
|
+
refreshing = undefined;
|
|
224
|
+
});
|
|
225
|
+
refreshing = { promise, forced: force };
|
|
226
|
+
return promise;
|
|
73
227
|
}
|
|
74
228
|
/** How long the outcome of a recovery is reused for: requests that fail together share one refresh, and a session that will not come back is not asked again at once. */
|
|
75
229
|
export const SESSION_RECOVERY_COOLDOWN_MS = 10 * 1000;
|
|
76
230
|
let recovery;
|
|
77
231
|
let lastRecovery;
|
|
78
232
|
/**
|
|
79
|
-
* Renews the session because a request was refused for lack of one, whatever this browser's record of the last refresh says
|
|
80
|
-
*
|
|
81
|
-
*
|
|
233
|
+
* Renews the session because a request was refused for lack of one, whatever this browser's record of the last refresh says - except that a refresh that
|
|
234
|
+
* succeeded within `SESSION_REFRESH_MIN_GAP_MS` (by this tab or another) is not repeated: the request was refused with the cookies from before it, so it is
|
|
235
|
+
* simply sent again with the new ones. Every request refused at about the same time shares one refresh, and its outcome is reused for `SESSION_RECOVERY_COOLDOWN_MS`.
|
|
236
|
+
* A refusal also clears the record of the last refresh, so the next regular check asks again (and sends the browser to sign-in if the refresh token is truly gone).
|
|
82
237
|
*
|
|
83
238
|
* @returns Whether the session was renewed.
|
|
84
239
|
*/
|
|
@@ -86,7 +241,7 @@ export function recoverSession(authServerUrl) {
|
|
|
86
241
|
if (lastRecovery && Date.now() - lastRecovery.at < SESSION_RECOVERY_COOLDOWN_MS) {
|
|
87
242
|
return Promise.resolve(lastRecovery.renewed);
|
|
88
243
|
}
|
|
89
|
-
recovery ?? (recovery = refreshSession(authServerUrl, true)
|
|
244
|
+
recovery ?? (recovery = refreshSession(authServerUrl, true, "recovery")
|
|
90
245
|
.then(() => true, () => {
|
|
91
246
|
writeTimestamp(() => localStorage, REFRESHED_AT_KEY, 0);
|
|
92
247
|
return false;
|
|
@@ -98,14 +253,13 @@ export function recoverSession(authServerUrl) {
|
|
|
98
253
|
}));
|
|
99
254
|
return recovery;
|
|
100
255
|
}
|
|
101
|
-
/** Forgets what recoveries have done (for tests). */
|
|
256
|
+
/** Forgets what recoveries and refreshes have done, and restores the retry delay and timeout (for tests). */
|
|
102
257
|
export function resetSessionRecovery() {
|
|
103
258
|
recovery = undefined;
|
|
104
259
|
lastRecovery = undefined;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
|
|
260
|
+
refreshing = undefined;
|
|
261
|
+
retryDelayMs = SESSION_REFRESH_RETRY_MS;
|
|
262
|
+
refreshTimeoutMs = SESSION_REFRESH_TIMEOUT_MS;
|
|
109
263
|
}
|
|
110
264
|
/**
|
|
111
265
|
* Redirects the browser to auth-server's sign-in page when `userUid` is absent, carrying the current URL as
|
|
@@ -127,7 +281,7 @@ export function useRedirectIfUnauthenticated(userUid, authServerUrl) {
|
|
|
127
281
|
console.error("Cannot redirect to sign-in: mail:auth_server_url is not configured.");
|
|
128
282
|
return;
|
|
129
283
|
}
|
|
130
|
-
|
|
284
|
+
goToSignIn(authServerUrl, "no-session");
|
|
131
285
|
}, [userUid, authServerUrl, explicitClient]);
|
|
132
286
|
}
|
|
133
287
|
/**
|
|
@@ -141,9 +295,11 @@ export function useRedirectIfUnauthenticated(userUid, authServerUrl) {
|
|
|
141
295
|
* - `userUid` set: also renews the session when a request is refused for lack of one (`apiFetch()` asks `recoverSession()` and sends the request
|
|
142
296
|
* again), so a token that ran out while timers were stopped costs nothing. Refreshes once the token is `SESSION_REFRESH_AFTER_MS` old (and right away when this browser has no
|
|
143
297
|
* record of a recent refresh, since the page's token age is unknown), checked every `SESSION_REFRESH_CHECK_MS` and
|
|
144
|
-
* whenever the tab becomes visible or the network returns, because a sleeping laptop stops timers
|
|
145
|
-
*
|
|
146
|
-
*
|
|
298
|
+
* whenever the tab becomes visible or focused or the network returns, because a sleeping laptop stops timers (a tab that wakes after the
|
|
299
|
+
* token's hour checks first, and a request refused meanwhile waits for that same refresh rather than starting its own). A failure that is
|
|
300
|
+
* not a rejection is simply retried at the next check. A rejection (`401`/`403` twice running, `SESSION_REFRESH_RETRY_MS` apart: the refresh
|
|
301
|
+
* token expired or was revoked, so the session cannot be kept) redirects to sign-in with `return_to` after `options.beforeRedirect` has run.
|
|
302
|
+
* Everything it does is recorded in the session log (`getSessionLog()`), which is also printed to the console when the browser goes to sign-in.
|
|
147
303
|
*
|
|
148
304
|
* **A no-op under an `ApiClientContext.Provider`** (see `util/apiClientContext.ts`): everything above assumes a
|
|
149
305
|
* `jwt`/`refresh` cookie pair this app can renew through auth-server's own `/auth/refresh` - meaningless for a
|
|
@@ -172,21 +328,26 @@ export function useSessionRefresh(userUid, authServerUrl, options = {}) {
|
|
|
172
328
|
}
|
|
173
329
|
let cancelled = false;
|
|
174
330
|
if (!userUid) {
|
|
175
|
-
if (paused
|
|
176
|
-
|
|
331
|
+
if (paused) {
|
|
332
|
+
goToSignIn(authServerUrl, "no-session paused");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (Date.now() - readTimestamp(() => sessionStorage, RELOADED_AT_KEY) < SESSION_RELOAD_GUARD_MS) {
|
|
336
|
+
goToSignIn(authServerUrl, "no-session after-reload");
|
|
177
337
|
return;
|
|
178
338
|
}
|
|
179
|
-
// Forced: the page has no session, so what this browser remembers of the last refresh says nothing about whether one is due.
|
|
180
|
-
refreshSession(authServerUrl, true)
|
|
339
|
+
// Forced: the page has no session, so what this browser remembers of the last refresh says nothing about whether one is due (but one made seconds ago is not repeated).
|
|
340
|
+
refreshSession(authServerUrl, true, "nouser")
|
|
181
341
|
.then(() => {
|
|
182
342
|
if (!cancelled) {
|
|
343
|
+
logSessionEvent("reload-after-refresh");
|
|
183
344
|
writeTimestamp(() => sessionStorage, RELOADED_AT_KEY, Date.now());
|
|
184
345
|
window.location.reload();
|
|
185
346
|
}
|
|
186
347
|
})
|
|
187
348
|
.catch(() => {
|
|
188
349
|
if (!cancelled) {
|
|
189
|
-
|
|
350
|
+
goToSignIn(authServerUrl, "no-session refresh-rejected");
|
|
190
351
|
}
|
|
191
352
|
});
|
|
192
353
|
return () => {
|
|
@@ -200,8 +361,10 @@ export function useSessionRefresh(userUid, authServerUrl, options = {}) {
|
|
|
200
361
|
setApiSessionRecovery(() => recoverSession(authServerUrl));
|
|
201
362
|
let leaving = false;
|
|
202
363
|
let inFlight = false;
|
|
203
|
-
|
|
364
|
+
let hiddenAt;
|
|
365
|
+
const leave = async (reason) => {
|
|
204
366
|
leaving = true;
|
|
367
|
+
logSessionEvent("leave:pending", reason);
|
|
205
368
|
try {
|
|
206
369
|
await beforeRedirectRef.current?.();
|
|
207
370
|
}
|
|
@@ -209,18 +372,22 @@ export function useSessionRefresh(userUid, authServerUrl, options = {}) {
|
|
|
209
372
|
// Saving what can be saved is best effort; the session is over either way.
|
|
210
373
|
}
|
|
211
374
|
if (!cancelled) {
|
|
212
|
-
|
|
375
|
+
goToSignIn(authServerUrl, reason);
|
|
213
376
|
}
|
|
214
377
|
};
|
|
215
|
-
|
|
378
|
+
// `trigger` names what woke the check, for the log; the periodic tick passes none, so it logs only when it actually refreshes.
|
|
379
|
+
const check = (trigger) => {
|
|
216
380
|
if (cancelled || leaving || inFlight) {
|
|
217
381
|
return;
|
|
218
382
|
}
|
|
383
|
+
if (trigger) {
|
|
384
|
+
logSessionEvent("check", trigger);
|
|
385
|
+
}
|
|
219
386
|
inFlight = true;
|
|
220
|
-
refreshSession(authServerUrl)
|
|
387
|
+
refreshSession(authServerUrl, false, "check")
|
|
221
388
|
.catch((err) => {
|
|
222
389
|
if (isAuthRejection(err)) {
|
|
223
|
-
void leave();
|
|
390
|
+
void leave("refresh-rejected");
|
|
224
391
|
}
|
|
225
392
|
})
|
|
226
393
|
.finally(() => {
|
|
@@ -228,20 +395,29 @@ export function useSessionRefresh(userUid, authServerUrl, options = {}) {
|
|
|
228
395
|
});
|
|
229
396
|
};
|
|
230
397
|
const onVisible = () => {
|
|
231
|
-
if (document.visibilityState === "
|
|
232
|
-
|
|
398
|
+
if (document.visibilityState === "hidden") {
|
|
399
|
+
hiddenAt = Date.now();
|
|
400
|
+
return;
|
|
233
401
|
}
|
|
402
|
+
logSessionEvent("visible", hiddenAt === undefined ? "hiddenFor=unknown" : `hiddenFor=${Date.now() - hiddenAt}ms`);
|
|
403
|
+
hiddenAt = undefined;
|
|
404
|
+
check("visible");
|
|
234
405
|
};
|
|
235
|
-
check();
|
|
236
|
-
const
|
|
406
|
+
const onFocus = () => check("focus");
|
|
407
|
+
const onOnline = () => check("online");
|
|
408
|
+
const onTick = () => check();
|
|
409
|
+
check("mount");
|
|
410
|
+
const interval = window.setInterval(onTick, SESSION_REFRESH_CHECK_MS);
|
|
237
411
|
document.addEventListener("visibilitychange", onVisible);
|
|
238
|
-
window.addEventListener("
|
|
412
|
+
window.addEventListener("focus", onFocus);
|
|
413
|
+
window.addEventListener("online", onOnline);
|
|
239
414
|
return () => {
|
|
240
415
|
cancelled = true;
|
|
241
416
|
setApiSessionRecovery(undefined);
|
|
242
417
|
window.clearInterval(interval);
|
|
243
418
|
document.removeEventListener("visibilitychange", onVisible);
|
|
244
|
-
window.removeEventListener("
|
|
419
|
+
window.removeEventListener("focus", onFocus);
|
|
420
|
+
window.removeEventListener("online", onOnline);
|
|
245
421
|
};
|
|
246
422
|
}, [userUid, authServerUrl, paused, explicitClient]);
|
|
247
423
|
}
|
|
@@ -38,10 +38,10 @@ export declare function updateBranding(input: UpdateBrandingInput, client?: ApiC
|
|
|
38
38
|
/** Uploads `file` as the logo, self-hosted via `BlobStore` — bypasses `apiFetch` (which always forces
|
|
39
39
|
* `Content-Type: application/json`) the same way `mailApi.ts`'s `uploadAttachment()` does, since
|
|
40
40
|
* `BaseBrandingRoute.uploadLogo()` reads the raw request body directly. */
|
|
41
|
-
export declare function uploadBrandingLogo(file: File): Promise<Branding>;
|
|
41
|
+
export declare function uploadBrandingLogo(file: File, client?: ApiClient): Promise<Branding>;
|
|
42
42
|
/** Uploads `file` as the compact nav-header icon, independently of `uploadBrandingLogo()`'s full logo. */
|
|
43
|
-
export declare function uploadBrandingIcon(file: File): Promise<Branding>;
|
|
44
|
-
export declare function uploadBrandingStylesheet(file: File): Promise<Branding>;
|
|
43
|
+
export declare function uploadBrandingIcon(file: File, client?: ApiClient): Promise<Branding>;
|
|
44
|
+
export declare function uploadBrandingStylesheet(file: File, client?: ApiClient): Promise<Branding>;
|
|
45
45
|
export declare function deleteBrandingLogo(client?: ApiClient): Promise<void>;
|
|
46
46
|
export declare function deleteBrandingIcon(client?: ApiClient): Promise<void>;
|
|
47
47
|
export declare function deleteBrandingStylesheet(client?: ApiClient): Promise<void>;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `GET /system/branding` needs no auth (every viewer, including an anonymous booking-page visitor, can read
|
|
9
9
|
* it); every write is `@RequiresTrustedRole()` on the backend.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { withClient, withClientRaw } from "../util/api.js";
|
|
12
12
|
/** `client`, given by every function below that isn't a raw-bytes upload, is an explicit `ApiClient` from
|
|
13
13
|
* `createApiClient()` (e.g. one account of a multi-account app) to call instead of the default global
|
|
14
14
|
* `apiFetch()` - see `withClient()`'s own doc comment in `util/api.ts`. Omitted (the default), every
|
|
@@ -22,33 +22,21 @@ export function updateBranding(input, client) {
|
|
|
22
22
|
body: JSON.stringify(input),
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
method: "POST",
|
|
28
|
-
credentials: "include",
|
|
29
|
-
headers: withCsrfHeader({ "Content-Type": file.type || "application/octet-stream" }),
|
|
30
|
-
body: file,
|
|
31
|
-
});
|
|
32
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
33
|
-
const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
34
|
-
if (!res.ok) {
|
|
35
|
-
const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
|
|
36
|
-
throw new ApiRequestError(message, res.status, responseBody?.code);
|
|
37
|
-
}
|
|
38
|
-
return responseBody;
|
|
25
|
+
function uploadBrandingAsset(path, file, client) {
|
|
26
|
+
return withClientRaw(client, path, "POST", file, file.type || "application/octet-stream");
|
|
39
27
|
}
|
|
40
28
|
/** Uploads `file` as the logo, self-hosted via `BlobStore` — bypasses `apiFetch` (which always forces
|
|
41
29
|
* `Content-Type: application/json`) the same way `mailApi.ts`'s `uploadAttachment()` does, since
|
|
42
30
|
* `BaseBrandingRoute.uploadLogo()` reads the raw request body directly. */
|
|
43
|
-
export function uploadBrandingLogo(file) {
|
|
44
|
-
return uploadBrandingAsset("/system/branding/logo", file);
|
|
31
|
+
export function uploadBrandingLogo(file, client) {
|
|
32
|
+
return uploadBrandingAsset("/system/branding/logo", file, client);
|
|
45
33
|
}
|
|
46
34
|
/** Uploads `file` as the compact nav-header icon, independently of `uploadBrandingLogo()`'s full logo. */
|
|
47
|
-
export function uploadBrandingIcon(file) {
|
|
48
|
-
return uploadBrandingAsset("/system/branding/icon", file);
|
|
35
|
+
export function uploadBrandingIcon(file, client) {
|
|
36
|
+
return uploadBrandingAsset("/system/branding/icon", file, client);
|
|
49
37
|
}
|
|
50
|
-
export function uploadBrandingStylesheet(file) {
|
|
51
|
-
return uploadBrandingAsset("/system/branding/stylesheet", file);
|
|
38
|
+
export function uploadBrandingStylesheet(file, client) {
|
|
39
|
+
return uploadBrandingAsset("/system/branding/stylesheet", file, client);
|
|
52
40
|
}
|
|
53
41
|
export function deleteBrandingLogo(client) {
|
|
54
42
|
return withClient(client, "/system/branding/logo", { method: "DELETE" });
|
|
@@ -24,6 +24,8 @@ export function visibilityOf(event) {
|
|
|
24
24
|
}
|
|
25
25
|
const LIST_PAGE_SIZE = 500;
|
|
26
26
|
const LIST_CALENDAR_EVENTS_MAX_PAGES = 100;
|
|
27
|
+
/** Without an order, a page boundary can skip or repeat an event: the `uid` is a fixed one. */
|
|
28
|
+
const EVENT_SORT = JSON.stringify({ uid: "ASC" });
|
|
27
29
|
/**
|
|
28
30
|
* Lists every event in a folder (same "fetch the flat list, filter client-side" contract as
|
|
29
31
|
* `contactsApi.ts`'s `listContacts`/`tasksApi.ts`'s `listTasks`), for the caller to expand and filter
|
|
@@ -43,7 +45,7 @@ const LIST_CALENDAR_EVENTS_MAX_PAGES = 100;
|
|
|
43
45
|
export async function listCalendarEvents(folderUid, client) {
|
|
44
46
|
const events = [];
|
|
45
47
|
for (let page = 0; page < LIST_CALENDAR_EVENTS_MAX_PAGES; page++) {
|
|
46
|
-
const batch = await withClient(client, `/mail/calendar-events?${buildQuery({ limit: LIST_PAGE_SIZE, page }, { folderUid })}`);
|
|
48
|
+
const batch = await withClient(client, `/mail/calendar-events?${buildQuery({ limit: LIST_PAGE_SIZE, page }, { folderUid, sort: EVENT_SORT })}`);
|
|
47
49
|
events.push(...batch);
|
|
48
50
|
if (batch.length < LIST_PAGE_SIZE) {
|
|
49
51
|
break;
|
|
@@ -64,6 +64,38 @@ export function searchOccurrences(events, query, now = new Date()) {
|
|
|
64
64
|
export function allOccurrences(events, now = new Date()) {
|
|
65
65
|
return occurrencesOf(events, () => true, now);
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The `count` occurrences of `chronological` (a series, in order of start, as `expandOccurrences()` returns it) that start nearest `now`, still in order. They
|
|
69
|
+
* are the run around `now`, so that is grown from it a side at a time (the nearer side first, the earlier one on a tie) rather than sorting the whole series.
|
|
70
|
+
*/
|
|
71
|
+
function nearest(chronological, now, count) {
|
|
72
|
+
const startOf = (o) => new Date(o.startDate).getTime();
|
|
73
|
+
// The first that starts at or after now.
|
|
74
|
+
let low = 0;
|
|
75
|
+
let high = chronological.length;
|
|
76
|
+
while (low < high) {
|
|
77
|
+
const middle = (low + high) >> 1;
|
|
78
|
+
if (startOf(chronological[middle]) < now) {
|
|
79
|
+
low = middle + 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
high = middle;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let from = low;
|
|
86
|
+
let to = low;
|
|
87
|
+
while (to - from < count) {
|
|
88
|
+
const before = from > 0 ? now - startOf(chronological[from - 1]) : Infinity;
|
|
89
|
+
const after = to < chronological.length ? startOf(chronological[to]) - now : Infinity;
|
|
90
|
+
if (before <= after) {
|
|
91
|
+
from--;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
to++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return chronological.slice(from, to);
|
|
98
|
+
}
|
|
67
99
|
/** The chronological occurrences of the `events` that `include` accepts (see `searchOccurrences()` for how a series is bounded). */
|
|
68
100
|
function occurrencesOf(events, include, now) {
|
|
69
101
|
const from = addYears(now, -SEARCH_WINDOW_YEARS);
|
|
@@ -73,16 +105,21 @@ function occurrencesOf(events, include, now) {
|
|
|
73
105
|
if (!include(event)) {
|
|
74
106
|
continue;
|
|
75
107
|
}
|
|
108
|
+
const once = () => expandOccurrences({ ...event, recurrenceRule: undefined }, new Date(new Date(event.startDate).getTime() - 1), new Date(new Date(event.endDate).getTime() + 1));
|
|
76
109
|
if (!event.recurrenceRule) {
|
|
77
|
-
found.push(...
|
|
110
|
+
found.push(...once());
|
|
78
111
|
continue;
|
|
79
112
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
let occurrences;
|
|
114
|
+
try {
|
|
115
|
+
occurrences = expandOccurrences(event, from, to);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// One event whose rule or dates cannot be expanded must not fail the whole search or agenda: it counts once, as the stored event.
|
|
119
|
+
found.push(...once());
|
|
120
|
+
continue;
|
|
84
121
|
}
|
|
85
|
-
found.push(...occurrences);
|
|
122
|
+
found.push(...(occurrences.length > SEARCH_MAX_OCCURRENCES_PER_SERIES ? nearest(occurrences, now.getTime(), SEARCH_MAX_OCCURRENCES_PER_SERIES) : occurrences));
|
|
86
123
|
}
|
|
87
124
|
return found.sort(byStart);
|
|
88
125
|
}
|
|
@@ -54,7 +54,8 @@ export interface PersonAvailability {
|
|
|
54
54
|
}
|
|
55
55
|
/** What a person's calendar says about a stretch of time. `"unknown"` is any person the server did not (or would not) tell about - never free. */
|
|
56
56
|
export type AvailabilityState = "free" | "tentative" | "busy" | "unknown";
|
|
57
|
-
/** The response's results with their times as milliseconds (a person who isn't `"available"` has no blocks whatever the server sent).
|
|
57
|
+
/** The response's results with their times as milliseconds (a person who isn't `"available"` has no blocks whatever the server sent). A window
|
|
58
|
+
* whose start or end can't be read is busy for all time - never silently free, which is what comparing `NaN` would make it. */
|
|
58
59
|
export declare function availabilityOf(response: FreeBusyResponse): PersonAvailability[];
|
|
59
60
|
/** What `person` is between `startMs` and `endMs`: `"busy"` when a firm block overlaps, else `"tentative"` when a tentative one does, else `"free"` - and
|
|
60
61
|
* `"unknown"`, whatever the blocks, for a person who isn't `"available"`. */
|
|
@@ -32,16 +32,22 @@ export function getFreeBusy(addresses, start, end, client) {
|
|
|
32
32
|
}),
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
/** The response's results with their times as milliseconds (a person who isn't `"available"` has no blocks whatever the server sent).
|
|
35
|
+
/** The response's results with their times as milliseconds (a person who isn't `"available"` has no blocks whatever the server sent). A window
|
|
36
|
+
* whose start or end can't be read is busy for all time - never silently free, which is what comparing `NaN` would make it. */
|
|
36
37
|
export function availabilityOf(response) {
|
|
37
38
|
return response.results.map((result) => ({
|
|
38
39
|
address: result.address,
|
|
39
40
|
status: result.status,
|
|
40
|
-
busy: result.status === "available"
|
|
41
|
-
? result.busy.map((window) => ({ startMs: Date.parse(window.start), endMs: Date.parse(window.end), tentative: window.tentative }))
|
|
42
|
-
: [],
|
|
41
|
+
busy: result.status === "available" ? result.busy.map(toBusyBlock) : [],
|
|
43
42
|
}));
|
|
44
43
|
}
|
|
44
|
+
function toBusyBlock(window) {
|
|
45
|
+
const startMs = Date.parse(window.start);
|
|
46
|
+
const endMs = Date.parse(window.end);
|
|
47
|
+
return Number.isFinite(startMs) && Number.isFinite(endMs)
|
|
48
|
+
? { startMs, endMs, tentative: window.tentative }
|
|
49
|
+
: { startMs: Number.NEGATIVE_INFINITY, endMs: Number.POSITIVE_INFINITY, tentative: window.tentative };
|
|
50
|
+
}
|
|
45
51
|
/** Whether a block overlaps the stretch from `startMs` to `endMs` (touching ends do not overlap). */
|
|
46
52
|
function overlaps(block, startMs, endMs) {
|
|
47
53
|
return block.startMs < endMs && block.endMs > startMs;
|
|
@@ -88,6 +94,8 @@ export function withoutOwnBlock(people, addresses, startMs, endMs) {
|
|
|
88
94
|
: person);
|
|
89
95
|
}
|
|
90
96
|
const DEFAULT_STEP_MS = 30 * 60000;
|
|
97
|
+
/** The finest step `suggestTimes()` takes: a zero or negative one would never advance, a tiny one would test millions of slots. */
|
|
98
|
+
const MIN_STEP_MS = 60000;
|
|
91
99
|
const DEFAULT_SUGGESTIONS = 5;
|
|
92
100
|
/**
|
|
93
101
|
* The start times (ms) of the first `limit` slots of `durationMs` inside `windows` where everyone known is free - no firm or tentative block overlaps it.
|
|
@@ -99,7 +107,7 @@ export function suggestTimes(people, options) {
|
|
|
99
107
|
if (known.length === 0 || options.durationMs <= 0) {
|
|
100
108
|
return [];
|
|
101
109
|
}
|
|
102
|
-
const step = options.stepMs
|
|
110
|
+
const step = Number.isFinite(options.stepMs) ? Math.max(MIN_STEP_MS, options.stepMs) : DEFAULT_STEP_MS;
|
|
103
111
|
const limit = options.limit ?? DEFAULT_SUGGESTIONS;
|
|
104
112
|
const found = [];
|
|
105
113
|
for (const window of [...options.windows].sort((a, b) => a.startMs - b.startMs)) {
|