@rapidmx/web-client 0.9.0 → 0.11.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/README.md +373 -88
- package/apps/admin/_layout.tsx +8 -2
- package/apps/admin/index.tsx +7 -1
- package/apps/admin/ingest-queue/index.tsx +2 -1
- package/apps/admin/mailboxes/[uid].tsx +23 -10
- package/apps/admin/quarantine/index.tsx +2 -1
- package/apps/admin/signing-certificates/index.tsx +343 -0
- package/apps/escrow/_layout.tsx +8 -2
- package/apps/shared/appearance/AppearanceForm.tsx +392 -0
- package/apps/shared/appearance/AppearanceHead.tsx +49 -0
- package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
- package/apps/shared/appearance/ColorField.tsx +107 -0
- package/apps/shared/appearance/appearanceCache.ts +106 -0
- package/apps/shared/appearance/appearanceContext.ts +77 -0
- package/apps/shared/appearance/bootScript.ts +35 -0
- package/apps/shared/appearance/color.ts +169 -0
- package/apps/shared/appearance/photo.ts +88 -0
- package/apps/shared/appearance/resolvedTheme.ts +67 -0
- package/apps/shared/appearance/theme.ts +355 -0
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
- package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
- package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
- package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactForm.tsx +11 -5
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
- package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
- package/apps/shared/components/layout/AppShell.tsx +477 -288
- package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
- package/apps/shared/components/layout/RailIcon.tsx +100 -0
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
- package/apps/shared/components/layout/UserMenu.tsx +372 -185
- package/apps/shared/components/mail/ConversationList.tsx +271 -262
- package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
- package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
- package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
- package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
- package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
- package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
- package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
- package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
- package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
- package/apps/shared/components/mail/reading/color.ts +245 -0
- package/apps/shared/components/mail/reading/frameControl.ts +142 -0
- package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
- package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
- package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
- package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
- package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
- package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
- package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +308 -0
- package/apps/shared/mail/folderTree.ts +143 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
- package/apps/shared/mail/outbox/composeBridge.ts +69 -0
- package/apps/shared/mail/outbox/outboxState.ts +88 -0
- package/apps/shared/mail/outbox/pendingSends.ts +146 -0
- package/apps/shared/mail/outbox/sendDecision.ts +157 -0
- package/apps/shared/mail/outbox/sendJob.ts +427 -0
- package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
- package/apps/shared/mail/outbox/sendState.ts +35 -0
- package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
- package/apps/shared/mail/useMailConnection.ts +203 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +178 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/notifications/NotificationCenter.tsx +215 -0
- package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
- package/apps/shared/notifications/apiErrors.ts +98 -0
- package/apps/shared/notifications/headerOffset.ts +35 -0
- package/apps/shared/notifications/pushStatus.ts +56 -0
- package/apps/shared/notifications/store.ts +549 -0
- package/apps/shared/notifications/systemErrors.ts +63 -0
- package/apps/shared/notifications/useNotifications.ts +41 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/signing/enrollmentStorage.ts +33 -0
- package/apps/shared/signing/enrollmentTracker.ts +385 -0
- package/apps/shared/signing/enrollmentView.ts +251 -0
- package/apps/shared/signing/signingInfo.ts +58 -0
- package/apps/shared/signing/useNow.ts +19 -0
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
- package/apps/shared/styles/app.css +108 -10
- package/apps/www/_layout.tsx +8 -2
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +36 -0
- package/apps/www/calendar/index.tsx +64 -33
- package/apps/www/contacts/[uid].tsx +109 -107
- package/apps/www/contacts/index.tsx +62 -42
- package/apps/www/index.tsx +2291 -1917
- package/apps/www/messages/[uid].tsx +108 -101
- package/apps/www/settings/appearance/index.tsx +21 -0
- package/apps/www/settings/auto-reply/index.tsx +7 -8
- package/apps/www/settings/encryption/index.tsx +1287 -1249
- package/apps/www/settings/filters/[uid].tsx +7 -2
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +140 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +7 -9
- package/apps/www/settings/read-receipts/index.tsx +7 -8
- package/apps/www/settings/sharing/index.tsx +256 -274
- package/apps/www/settings/signatures/[uid].tsx +172 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +135 -129
- package/apps/www/tasks/index.tsx +29 -18
- package/dist/apps/admin/_layout.d.ts +5 -1
- package/dist/apps/admin/_layout.js +3 -2
- package/dist/apps/admin/index.js +3 -2
- package/dist/apps/admin/ingest-queue/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +9 -6
- package/dist/apps/admin/quarantine/index.js +2 -1
- package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
- package/dist/apps/admin/signing-certificates/index.js +161 -0
- package/dist/apps/escrow/_layout.d.ts +5 -1
- package/dist/apps/escrow/_layout.js +3 -2
- package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
- package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
- package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
- package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
- package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
- package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
- package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
- package/dist/apps/shared/appearance/ColorField.js +40 -0
- package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceCache.js +80 -0
- package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceContext.js +29 -0
- package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
- package/dist/apps/shared/appearance/bootScript.js +33 -0
- package/dist/apps/shared/appearance/color.d.ts +65 -0
- package/dist/apps/shared/appearance/color.js +132 -0
- package/dist/apps/shared/appearance/photo.d.ts +17 -0
- package/dist/apps/shared/appearance/photo.js +79 -0
- package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
- package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
- package/dist/apps/shared/appearance/theme.d.ts +111 -0
- package/dist/apps/shared/appearance/theme.js +251 -0
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
- package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
- package/dist/apps/shared/components/layout/AppShell.js +122 -17
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
- package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
- package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
- package/dist/apps/shared/components/layout/RailIcon.js +85 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
- package/dist/apps/shared/components/layout/UserMenu.js +88 -7
- package/dist/apps/shared/components/mail/ConversationList.js +5 -11
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
- package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
- package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
- package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
- package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
- package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
- package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
- package/dist/apps/shared/components/mail/reading/color.js +201 -0
- package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
- package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +218 -0
- package/dist/apps/shared/mail/folderTree.d.ts +55 -0
- package/dist/apps/shared/mail/folderTree.js +116 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
- package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
- package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
- package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
- package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
- package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
- package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
- package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
- package/dist/apps/shared/mail/outbox/sendState.js +8 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
- package/dist/apps/shared/mail/useMailConnection.js +138 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
- package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
- package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
- package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
- package/dist/apps/shared/notifications/apiErrors.js +86 -0
- package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
- package/dist/apps/shared/notifications/headerOffset.js +33 -0
- package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
- package/dist/apps/shared/notifications/pushStatus.js +54 -0
- package/dist/apps/shared/notifications/store.d.ts +136 -0
- package/dist/apps/shared/notifications/store.js +416 -0
- package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
- package/dist/apps/shared/notifications/systemErrors.js +58 -0
- package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
- package/dist/apps/shared/notifications/useNotifications.js +18 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
- package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
- package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
- package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
- package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
- package/dist/apps/shared/signing/enrollmentView.js +214 -0
- package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
- package/dist/apps/shared/signing/signingInfo.js +48 -0
- package/dist/apps/shared/signing/useNow.d.ts +2 -0
- package/dist/apps/shared/signing/useNow.js +18 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
- package/dist/apps/shared/styles/app.css +108 -10
- package/dist/apps/www/_layout.d.ts +5 -1
- package/dist/apps/www/_layout.js +3 -2
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +30 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +41 -11
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +11 -9
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +30 -24
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +332 -48
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +10 -5
- package/dist/apps/www/settings/appearance/index.d.ts +4 -0
- package/dist/apps/www/settings/appearance/index.js +13 -0
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +6 -7
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +106 -96
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +6 -2
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +9 -3
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +8 -9
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +6 -7
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +22 -34
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +6 -2
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +9 -4
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +22 -16
- package/package.json +2 -2
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { MIN_TEXT_CONTRAST, adaptForeground, blackOrWhite, composite, contrastRatio, ensureContrast, firstColourStop, formatColour, luminance, parseColour, sameColour, } from "./color.js";
|
|
6
|
+
import { LINK_SENTINEL, TEXT_SENTINEL } from "./frameDocument.js";
|
|
7
|
+
import { documentElementOf, viewOf } from "./safeDocument.js";
|
|
8
|
+
/** Not examined, nor are their children. `head` and its contents never render; `svg` is left as drawn. */
|
|
9
|
+
const SKIPPED = new Set(["head", "script", "style", "title", "meta", "link", "template", "svg", "img"]);
|
|
10
|
+
/** Classic link blue - what an unauthored link looks like in an authored region, before contrast. */
|
|
11
|
+
const DEFAULT_LINK = { r: 0, g: 0, b: 238 };
|
|
12
|
+
/** How much lighter or darker (in luminance) an adapted text colour must be than the original for the difference to be worth an opt-out. */
|
|
13
|
+
const MATERIAL_LUMINANCE_SHIFT = 0.3;
|
|
14
|
+
/** A surface at least this luminous is close enough to the white a message is authored on that it is not, by itself, an adaptation. */
|
|
15
|
+
const LIGHT_SURFACE = 0.85;
|
|
16
|
+
/** The elements whose background is the message's canvas: `<html>`, `<body>`, and the wrapper that stands in for the message's own `<body>`. */
|
|
17
|
+
function standsForCanvas(element, root) {
|
|
18
|
+
return element === root || element.localName === "body" || element.parentElement?.id === "rr-body";
|
|
19
|
+
}
|
|
20
|
+
function noop() {
|
|
21
|
+
// Nothing was written, so there is nothing to put back.
|
|
22
|
+
}
|
|
23
|
+
function nextTurn() {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
25
|
+
}
|
|
26
|
+
/** The colour a `color` value comes to over a background: opaque, alpha composited. */
|
|
27
|
+
function opaque(css, over) {
|
|
28
|
+
const colour = parseColour(css);
|
|
29
|
+
return colour ? composite(colour, over) : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Adapts `doc` to `surface` - see the file's header. Time-sliced (`sliceMs`, then a turn for the browser) so a very large message never
|
|
33
|
+
* blocks the page; the frame stays hidden until this resolves, so nothing unreadable is ever shown.
|
|
34
|
+
*/
|
|
35
|
+
export async function adaptDocument(doc, surface, options = {}) {
|
|
36
|
+
const { sliceMs = 12, isCancelled = () => false, now = () => performance.now(), yieldToBrowser = nextTurn } = options;
|
|
37
|
+
const view = viewOf(doc);
|
|
38
|
+
const result = { elements: 0, written: 0, material: false, authoredCanvas: false, cancelled: false, revert: noop, reapply: noop };
|
|
39
|
+
if (!view) {
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
const root = documentElementOf(doc);
|
|
43
|
+
// Every read happens before the first write: a write changes what its descendants inherit, which would make every child of an element
|
|
44
|
+
// we coloured look authored.
|
|
45
|
+
const writes = [];
|
|
46
|
+
const stack = [
|
|
47
|
+
{ element: root, context: { background: surface.background, authored: false, unknown: false, parentColour: surface.text } },
|
|
48
|
+
];
|
|
49
|
+
let sliceStart = now();
|
|
50
|
+
while (stack.length > 0) {
|
|
51
|
+
if (now() - sliceStart > sliceMs) {
|
|
52
|
+
await yieldToBrowser();
|
|
53
|
+
if (isCancelled()) {
|
|
54
|
+
result.cancelled = true;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
sliceStart = now();
|
|
58
|
+
}
|
|
59
|
+
const { element, context } = stack.pop();
|
|
60
|
+
if (SKIPPED.has(element.localName.toLowerCase())) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
result.elements++;
|
|
64
|
+
const style = view.getComputedStyle(element);
|
|
65
|
+
let { background, authored, unknown } = context;
|
|
66
|
+
const own = parseColour(style.backgroundColor);
|
|
67
|
+
const image = style.backgroundImage;
|
|
68
|
+
if (image && image !== "none") {
|
|
69
|
+
const stop = firstColourStop(image) ?? (own && own.a > 0 ? own : undefined);
|
|
70
|
+
authored = true;
|
|
71
|
+
unknown = !stop;
|
|
72
|
+
background = stop ? composite(stop, background) : background;
|
|
73
|
+
result.authoredCanvas || (result.authoredCanvas = standsForCanvas(element, root));
|
|
74
|
+
}
|
|
75
|
+
else if (own && own.a >= 0.98) {
|
|
76
|
+
authored = true;
|
|
77
|
+
unknown = false;
|
|
78
|
+
background = composite(own, background);
|
|
79
|
+
result.authoredCanvas || (result.authoredCanvas = standsForCanvas(element, root));
|
|
80
|
+
}
|
|
81
|
+
else if (own && own.a > 0) {
|
|
82
|
+
background = composite(own, background);
|
|
83
|
+
}
|
|
84
|
+
const declared = style.color;
|
|
85
|
+
const original = opaque(declared, background);
|
|
86
|
+
let target;
|
|
87
|
+
if (declared === TEXT_SENTINEL) {
|
|
88
|
+
const rgb = authored ? (unknown ? { r: 0, g: 0, b: 0 } : blackOrWhite(background)) : ensureContrast(surface.text, background);
|
|
89
|
+
target = { rgb, css: formatColour(rgb) };
|
|
90
|
+
}
|
|
91
|
+
else if (declared === LINK_SENTINEL) {
|
|
92
|
+
const rgb = authored ? (unknown ? DEFAULT_LINK : ensureContrast(DEFAULT_LINK, background)) : ensureContrast(surface.link, background);
|
|
93
|
+
target = { rgb, css: formatColour(rgb) };
|
|
94
|
+
}
|
|
95
|
+
else if (original && !authored) {
|
|
96
|
+
// Authored text on the theme's surface: it stays if it reads, else it is adapted.
|
|
97
|
+
const readable = contrastRatio(original, background) >= MIN_TEXT_CONTRAST;
|
|
98
|
+
const rgb = readable ? original : adaptForeground(original, { ...surface, background });
|
|
99
|
+
target = { rgb, css: readable ? declared : formatColour(rgb) };
|
|
100
|
+
}
|
|
101
|
+
else if (original) {
|
|
102
|
+
// Authored text on an authored background: as its author had it.
|
|
103
|
+
target = { rgb: original, css: declared };
|
|
104
|
+
}
|
|
105
|
+
let parentColour = context.parentColour;
|
|
106
|
+
if (target) {
|
|
107
|
+
// An element is written to unless inheritance already gives it its target: the parent must end with that colour, and an element
|
|
108
|
+
// that may carry a declaration of its own (an authored colour, a link - the sentinel is a rule on it - and the root) must not
|
|
109
|
+
// be declaring a different one. Text nobody coloured inherits and has nothing of its own to override.
|
|
110
|
+
const ownDeclaration = declared !== TEXT_SENTINEL || element === root;
|
|
111
|
+
if (!sameColour(target.rgb, context.parentColour) || (ownDeclaration && !sameColour(target.rgb, original ?? target.rgb))) {
|
|
112
|
+
writes.push({ element, colour: target.css });
|
|
113
|
+
}
|
|
114
|
+
if (original && Math.abs(luminance(target.rgb) - luminance(original)) > MATERIAL_LUMINANCE_SHIFT) {
|
|
115
|
+
result.material = true;
|
|
116
|
+
}
|
|
117
|
+
parentColour = target.rgb;
|
|
118
|
+
}
|
|
119
|
+
for (const child of Array.from(element.children).reverse()) {
|
|
120
|
+
stack.push({ element: child, context: { background, authored, unknown, parentColour } });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// What each element had inline before, so the writes can be taken back (a message may carry its own inline colour).
|
|
124
|
+
const before = writes.map(({ element }) => ({ value: element.style.getPropertyValue("color"), priority: element.style.getPropertyPriority("color") }));
|
|
125
|
+
result.reapply = () => {
|
|
126
|
+
for (const { element, colour } of writes) {
|
|
127
|
+
element.style.setProperty("color", colour, "important");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
result.revert = () => {
|
|
131
|
+
writes.forEach(({ element }, index) => {
|
|
132
|
+
if (before[index].value) {
|
|
133
|
+
element.style.setProperty("color", before[index].value, before[index].priority);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
element.style.removeProperty("color");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
result.reapply();
|
|
141
|
+
result.written = writes.length;
|
|
142
|
+
result.material || (result.material = !result.authoredCanvas && luminance(surface.background) < LIGHT_SURFACE);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ResolvedTheme } from "../../../appearance/resolvedTheme.js";
|
|
2
|
+
import { ThemeSurface } from "./color.js";
|
|
3
|
+
/** The palette used where the page's own tokens cannot be read (no DOM, or a stylesheet without them): the app's built-in schemes. */
|
|
4
|
+
export declare const DEFAULT_SURFACES: Record<ResolvedTheme, ThemeSurface>;
|
|
5
|
+
/**
|
|
6
|
+
* The theme a message is shown in, read from the page's own tokens: the surface behind the body composited to an opaque colour (a
|
|
7
|
+
* translucent surface over a photo would make text unreadable, so the body area is always painted with this opaque one), the text
|
|
8
|
+
* colour and the link colour. Falls back to the built-in scheme for anything the page doesn't define.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readThemeSurface(theme: ResolvedTheme): ThemeSurface;
|
|
11
|
+
/** A string that changes exactly when the surface does - what a frame is keyed on. */
|
|
12
|
+
export declare function surfaceKey(surface: ThemeSurface): string;
|
|
13
|
+
/**
|
|
14
|
+
* The theme surface for `theme`, kept current: re-read when the scheme changes and when the page's own colours are changed at runtime
|
|
15
|
+
* (Appearance sets `data-theme` on `<html>` and writes the user's colours into a `<style>` in the head). The identity only changes when a colour does.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useThemeSurface(theme: ResolvedTheme): ThemeSurface;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { composite, formatColour, parseColour } from "./color.js";
|
|
7
|
+
/** The palette used where the page's own tokens cannot be read (no DOM, or a stylesheet without them): the app's built-in schemes. */
|
|
8
|
+
export const DEFAULT_SURFACES = {
|
|
9
|
+
light: { background: { r: 255, g: 255, b: 255 }, text: { r: 28, g: 37, b: 38 }, link: { r: 20, g: 138, b: 128 }, dark: false },
|
|
10
|
+
dark: { background: { r: 27, g: 32, b: 34 }, text: { r: 238, g: 242, b: 243 }, link: { r: 45, g: 212, b: 191 }, dark: true },
|
|
11
|
+
};
|
|
12
|
+
/** The page's own CSS variables a message is adapted to. Read from the document, so a branding palette or a user's own Appearance colours are followed. */
|
|
13
|
+
const TOKENS = {
|
|
14
|
+
surface: "--rr-color-surface",
|
|
15
|
+
page: "--rr-color-bg",
|
|
16
|
+
text: "--rr-color-text",
|
|
17
|
+
link: "--rr-color-primary-dark",
|
|
18
|
+
};
|
|
19
|
+
/** The colour a CSS custom property resolves to, by asking the browser what a probe element painted with it comes to (which also
|
|
20
|
+
* resolves `var()` chains and colour functions); `undefined` when it isn't defined or isn't a colour. */
|
|
21
|
+
function resolveToken(probe, name) {
|
|
22
|
+
probe.style.color = `var(${name}, transparent)`;
|
|
23
|
+
const colour = parseColour(getComputedStyle(probe).color);
|
|
24
|
+
return colour && colour.a > 0 ? colour : undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The theme a message is shown in, read from the page's own tokens: the surface behind the body composited to an opaque colour (a
|
|
28
|
+
* translucent surface over a photo would make text unreadable, so the body area is always painted with this opaque one), the text
|
|
29
|
+
* colour and the link colour. Falls back to the built-in scheme for anything the page doesn't define.
|
|
30
|
+
*/
|
|
31
|
+
export function readThemeSurface(theme) {
|
|
32
|
+
const fallback = DEFAULT_SURFACES[theme];
|
|
33
|
+
if (typeof document === "undefined" || !document.body) {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
const probe = document.createElement("span");
|
|
37
|
+
probe.style.display = "none";
|
|
38
|
+
document.body.appendChild(probe);
|
|
39
|
+
try {
|
|
40
|
+
const page = resolveToken(probe, TOKENS.page);
|
|
41
|
+
const surface = resolveToken(probe, TOKENS.surface);
|
|
42
|
+
const underlay = page ? composite(page, fallback.background) : fallback.background;
|
|
43
|
+
const text = resolveToken(probe, TOKENS.text);
|
|
44
|
+
const link = resolveToken(probe, TOKENS.link);
|
|
45
|
+
return {
|
|
46
|
+
dark: fallback.dark,
|
|
47
|
+
background: surface ? composite(surface, underlay) : underlay,
|
|
48
|
+
text: text ? composite(text, underlay) : fallback.text,
|
|
49
|
+
link: link ? composite(link, underlay) : fallback.link,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
probe.remove();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** A string that changes exactly when the surface does - what a frame is keyed on. */
|
|
57
|
+
export function surfaceKey(surface) {
|
|
58
|
+
return [surface.background, surface.text, surface.link].map(formatColour).join("|") + (surface.dark ? "|dark" : "|light");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The theme surface for `theme`, kept current: re-read when the scheme changes and when the page's own colours are changed at runtime
|
|
62
|
+
* (Appearance sets `data-theme` on `<html>` and writes the user's colours into a `<style>` in the head). The identity only changes when a colour does.
|
|
63
|
+
*/
|
|
64
|
+
export function useThemeSurface(theme) {
|
|
65
|
+
const [surface, setSurface] = useState(() => readThemeSurface(theme));
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const update = () => setSurface((previous) => {
|
|
68
|
+
const next = readThemeSurface(theme);
|
|
69
|
+
return surfaceKey(previous) === surfaceKey(next) ? previous : next;
|
|
70
|
+
});
|
|
71
|
+
update();
|
|
72
|
+
if (typeof MutationObserver === "undefined") {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const observer = new MutationObserver(update);
|
|
76
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["style", "class", "data-theme"] });
|
|
77
|
+
// The Appearance provider writes the user's own colours into a `<style>` element in the head, whose text is replaced when they change.
|
|
78
|
+
observer.observe(document.head, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
79
|
+
return () => observer.disconnect();
|
|
80
|
+
}, [theme]);
|
|
81
|
+
// The render that first sees a new scheme must not use the previous one's colours (the effect above corrects the state after it).
|
|
82
|
+
return surface.dark === (theme === "dark") ? surface : readThemeSurface(theme);
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Whether the reader asked to see `uid` as authored. */
|
|
2
|
+
export declare function isViewedOriginal(uid: string): boolean;
|
|
3
|
+
/** Forgets every choice (the tests). */
|
|
4
|
+
export declare function clearViewedOriginal(): void;
|
|
5
|
+
/**
|
|
6
|
+
* The per-message "View original" choice: `false` (follow the theme) until the reader flips it, then remembered for the session so
|
|
7
|
+
* collapsing and expanding a message, or coming back to it, keeps the choice.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useViewOriginal(uid: string): [boolean, (value: boolean) => void];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useCallback, useState } from "react";
|
|
6
|
+
/** Messages the reader chose to see exactly as authored rather than adapted to the theme, for the life of the tab. */
|
|
7
|
+
const original = new Set();
|
|
8
|
+
/** Whether the reader asked to see `uid` as authored. */
|
|
9
|
+
export function isViewedOriginal(uid) {
|
|
10
|
+
return original.has(uid);
|
|
11
|
+
}
|
|
12
|
+
/** Forgets every choice (the tests). */
|
|
13
|
+
export function clearViewedOriginal() {
|
|
14
|
+
original.clear();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The per-message "View original" choice: `false` (follow the theme) until the reader flips it, then remembered for the session so
|
|
18
|
+
* collapsing and expanding a message, or coming back to it, keeps the choice.
|
|
19
|
+
*/
|
|
20
|
+
export function useViewOriginal(uid) {
|
|
21
|
+
const [value, setValue] = useState(() => isViewedOriginal(uid));
|
|
22
|
+
const update = useCallback((next) => {
|
|
23
|
+
if (next) {
|
|
24
|
+
original.add(uid);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
original.delete(uid);
|
|
28
|
+
}
|
|
29
|
+
setValue(next);
|
|
30
|
+
}, [uid]);
|
|
31
|
+
return [value, update];
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
/**
|
|
4
|
+
* How an unread message looks in every list of mail (the message list, the conversation list and its child rows, the
|
|
5
|
+
* thread pane's message headers, the folder badges), in one place so they can't drift apart. Modelled on Outlook:
|
|
6
|
+
*
|
|
7
|
+
* **Unread**: an accent bar down the row's left edge, a faint accent tint on the row, the sender and subject in bold and the
|
|
8
|
+
* date in the accent colour - plus a visually-hidden "Unread" in the row's text, so it never relies on colour alone.
|
|
9
|
+
*
|
|
10
|
+
* **Read**: normal weight, the sender and preview muted.
|
|
11
|
+
*
|
|
12
|
+
* **Hover, selected and focused** are their own states and look nothing like unread: hover is a plain grey (an accent tint on
|
|
13
|
+
* an unread row, so the row doesn't seem to lose its state), the open row has a stronger accent fill and an inset outline, and
|
|
14
|
+
* keyboard focus is a solid outline drawn inside the row.
|
|
15
|
+
*
|
|
16
|
+
* All of it is built from the theme's own tokens (`primary`, `text`, `text-muted`, `surface-alt`), so it follows light and
|
|
17
|
+
* dark mode and a branding stylesheet's palette.
|
|
18
|
+
*/
|
|
19
|
+
/** Whether a message is unread: `flags.read` isn't `true`. A message with no `read` flag at all counts as unread. */
|
|
20
|
+
export declare function isUnread(message: Pick<Message, "flags"> | undefined | null): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The classes of a row's outer element (`li`, or the row's wrapping `div`): the positioning the bar needs, and its one
|
|
23
|
+
* background - the open row's accent fill and inset outline, else an unread row's faint tint (a little stronger on hover),
|
|
24
|
+
* else a read row's plain grey hover. One element owns all of it so the states can't fight over the same property.
|
|
25
|
+
*/
|
|
26
|
+
export declare function rowClass(state: {
|
|
27
|
+
unread: boolean;
|
|
28
|
+
selected: boolean;
|
|
29
|
+
}, extra?: string): string;
|
|
30
|
+
/** Keyboard focus on a row's button: a solid outline drawn inside the row, unlike anything unread/selected/hover does. */
|
|
31
|
+
export declare const ROW_FOCUS_CLASS = "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary";
|
|
32
|
+
/** The sender line: bold and full-strength while unread, normal and muted once read. */
|
|
33
|
+
export declare function senderClass(unread: boolean): string;
|
|
34
|
+
/** The subject line. */
|
|
35
|
+
export declare function subjectClass(unread: boolean): string;
|
|
36
|
+
/** The date beside the sender: the accent colour while unread. */
|
|
37
|
+
export declare function dateClass(unread: boolean): string;
|
|
38
|
+
/** The accent bar down a row's left edge - only for an unread row. The row's outer element must be `relative`. */
|
|
39
|
+
export declare function UnreadBar({ unread }: {
|
|
40
|
+
unread: boolean;
|
|
41
|
+
}): React.JSX.Element | null;
|
|
42
|
+
/** "Unread." in the row's text for assistive technology (and for anything that reads the DOM without styles). */
|
|
43
|
+
export declare function UnreadLabel({ unread }: {
|
|
44
|
+
unread: boolean;
|
|
45
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* How an unread message looks in every list of mail (the message list, the conversation list and its child rows, the
|
|
4
|
+
* thread pane's message headers, the folder badges), in one place so they can't drift apart. Modelled on Outlook:
|
|
5
|
+
*
|
|
6
|
+
* **Unread**: an accent bar down the row's left edge, a faint accent tint on the row, the sender and subject in bold and the
|
|
7
|
+
* date in the accent colour - plus a visually-hidden "Unread" in the row's text, so it never relies on colour alone.
|
|
8
|
+
*
|
|
9
|
+
* **Read**: normal weight, the sender and preview muted.
|
|
10
|
+
*
|
|
11
|
+
* **Hover, selected and focused** are their own states and look nothing like unread: hover is a plain grey (an accent tint on
|
|
12
|
+
* an unread row, so the row doesn't seem to lose its state), the open row has a stronger accent fill and an inset outline, and
|
|
13
|
+
* keyboard focus is a solid outline drawn inside the row.
|
|
14
|
+
*
|
|
15
|
+
* All of it is built from the theme's own tokens (`primary`, `text`, `text-muted`, `surface-alt`), so it follows light and
|
|
16
|
+
* dark mode and a branding stylesheet's palette.
|
|
17
|
+
*/
|
|
18
|
+
/** Whether a message is unread: `flags.read` isn't `true`. A message with no `read` flag at all counts as unread. */
|
|
19
|
+
export function isUnread(message) {
|
|
20
|
+
return !!message && message.flags.read !== true;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The classes of a row's outer element (`li`, or the row's wrapping `div`): the positioning the bar needs, and its one
|
|
24
|
+
* background - the open row's accent fill and inset outline, else an unread row's faint tint (a little stronger on hover),
|
|
25
|
+
* else a read row's plain grey hover. One element owns all of it so the states can't fight over the same property.
|
|
26
|
+
*/
|
|
27
|
+
export function rowClass(state, extra = "") {
|
|
28
|
+
return [
|
|
29
|
+
"relative border-b border-border",
|
|
30
|
+
state.selected
|
|
31
|
+
? "bg-primary/20 ring-1 ring-inset ring-primary/50"
|
|
32
|
+
: state.unread
|
|
33
|
+
? "bg-primary/[0.07] hover:bg-primary/10"
|
|
34
|
+
: "hover:bg-surface-alt",
|
|
35
|
+
extra,
|
|
36
|
+
]
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(" ");
|
|
39
|
+
}
|
|
40
|
+
/** Keyboard focus on a row's button: a solid outline drawn inside the row, unlike anything unread/selected/hover does. */
|
|
41
|
+
export const ROW_FOCUS_CLASS = "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary";
|
|
42
|
+
/** The sender line: bold and full-strength while unread, normal and muted once read. */
|
|
43
|
+
export function senderClass(unread) {
|
|
44
|
+
return unread ? "font-bold text-text" : "font-normal text-text-muted";
|
|
45
|
+
}
|
|
46
|
+
/** The subject line. */
|
|
47
|
+
export function subjectClass(unread) {
|
|
48
|
+
return unread ? "font-semibold text-text" : "font-normal text-text";
|
|
49
|
+
}
|
|
50
|
+
/** The date beside the sender: the accent colour while unread. */
|
|
51
|
+
export function dateClass(unread) {
|
|
52
|
+
return unread ? "font-semibold text-primary-dark" : "font-normal text-text-muted";
|
|
53
|
+
}
|
|
54
|
+
/** The accent bar down a row's left edge - only for an unread row. The row's outer element must be `relative`. */
|
|
55
|
+
export function UnreadBar({ unread }) {
|
|
56
|
+
return unread ? _jsx("span", { "aria-hidden": "true", "data-unread-bar": true, className: "absolute inset-y-0 left-0 w-1 bg-primary" }) : null;
|
|
57
|
+
}
|
|
58
|
+
/** "Unread." in the row's text for assistive technology (and for anything that reads the DOM without styles). */
|
|
59
|
+
export function UnreadLabel({ unread }) {
|
|
60
|
+
return unread ? _jsx("span", { className: "sr-only", children: "Unread. " }) : null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { EnrollmentResult } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
3
|
+
import type { EnrollmentSnapshot } from "../../signing/enrollmentTracker.js";
|
|
4
|
+
import type { SigningEnrollmentInfo } from "../../signing/signingInfo.js";
|
|
5
|
+
export type SigningCertificateMode = "pending" | "issued" | "failed" | "expired";
|
|
6
|
+
export interface SigningCertificateCardProps {
|
|
7
|
+
mode: SigningCertificateMode;
|
|
8
|
+
/** What the server last said about the enrollment (the extended fields are all optional - an older server sends only `status`). */
|
|
9
|
+
enrollment: EnrollmentResult | null;
|
|
10
|
+
/** The tracker's view of it: busy flags, the check cooldown, the last check. Absent when nothing is being followed. */
|
|
11
|
+
snapshot?: EnrollmentSnapshot;
|
|
12
|
+
/** For an issued certificate when the enrollment carries no details (one installed long ago): the active key's own expiry (epoch ms). */
|
|
13
|
+
keyNotAfter?: number;
|
|
14
|
+
/** How this deployment issues certificates (`GET /system/signing-enrollment`): what a pending request says follows it. `null` / absent when it is not known. */
|
|
15
|
+
info?: SigningEnrollmentInfo | null;
|
|
16
|
+
/** The mailbox's address - the one the CA's verification e-mail goes to. */
|
|
17
|
+
address?: string;
|
|
18
|
+
/** Only the mailbox's owner can request a certificate. */
|
|
19
|
+
canRequest: boolean;
|
|
20
|
+
/** "Try again" / "Renew" / "Request a new certificate" is under way. */
|
|
21
|
+
requesting: boolean;
|
|
22
|
+
onCheck?: () => void;
|
|
23
|
+
onRequest?: () => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Settings > Encryption's "Digital signature certificate" card: where a signing-certificate enrollment is, in the words of the person waiting for
|
|
27
|
+
* it. A progress bar (determinate from the server's `progress`), the steps with a state icon and time each, a status line, when it was requested
|
|
28
|
+
* and last checked (kept current), and a "Check status" button with a busy state and a cooldown. An issued certificate shows its subject, issuer,
|
|
29
|
+
* serial (copyable) and expiry with a warning (and "Renew") inside 30 days; a failed one shows the reason and "Try again"; an expired one offers a
|
|
30
|
+
* new request. Everything but `mode` is optional so a server that only says `pending`/`issued`/`failed` still gets a useful card.
|
|
31
|
+
*/
|
|
32
|
+
export default function SigningCertificateCard({ mode, enrollment, snapshot, keyNotAfter, info, address, canRequest, requesting, onCheck, onRequest }: SigningCertificateCardProps): React.JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineCheckCircle, HiOutlineExclamationTriangle, HiOutlineXCircle } from "react-icons/hi2";
|
|
3
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
4
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
5
|
+
import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
6
|
+
import { CA_UNREACHABLE_TEXT, MANUAL_ISSUE_TEXT, PENDING_FALLBACK_TEXT, expiryOf, formatDate, formatDateTime, healthWarning, isInstalling, isOverdue, issueModeOf, progressOf, requestedFromText, relativeTime, statusLineOf, stepsOf, timeOf, truncateSerial, } from "../../signing/enrollmentView.js";
|
|
7
|
+
import { useNow } from "../../signing/useNow.js";
|
|
8
|
+
const BADGE = {
|
|
9
|
+
pending: { text: "Pending", className: "bg-primary/15 text-primary-dark" },
|
|
10
|
+
issued: { text: "Active", className: "bg-success/15 text-success" },
|
|
11
|
+
failed: { text: "Failed", className: "bg-danger/15 text-danger" },
|
|
12
|
+
expired: { text: "Expired", className: "bg-danger/15 text-danger" },
|
|
13
|
+
};
|
|
14
|
+
const STEP_TEXT = {
|
|
15
|
+
done: "done",
|
|
16
|
+
active: "in progress",
|
|
17
|
+
pending: "waiting",
|
|
18
|
+
failed: "failed",
|
|
19
|
+
};
|
|
20
|
+
function StepIcon({ state }) {
|
|
21
|
+
if (state === "done") {
|
|
22
|
+
return _jsx(HiOutlineCheckCircle, { size: 18, "aria-hidden": "true", className: "shrink-0 text-success" });
|
|
23
|
+
}
|
|
24
|
+
if (state === "failed") {
|
|
25
|
+
return _jsx(HiOutlineXCircle, { size: 18, "aria-hidden": "true", className: "shrink-0 text-danger" });
|
|
26
|
+
}
|
|
27
|
+
if (state === "active") {
|
|
28
|
+
return (_jsx("span", { "aria-hidden": "true", className: "flex h-[18px] w-[18px] shrink-0 items-center justify-center", children: _jsx("span", { className: "h-3.5 w-3.5 rounded-full border-2 border-primary/30 border-t-primary motion-safe:animate-spin motion-reduce:border-primary" }) }));
|
|
29
|
+
}
|
|
30
|
+
return (_jsx("span", { "aria-hidden": "true", className: "flex h-[18px] w-[18px] shrink-0 items-center justify-center", children: _jsx("span", { className: "h-3 w-3 rounded-full border-2 border-border" }) }));
|
|
31
|
+
}
|
|
32
|
+
function Stepper({ steps }) {
|
|
33
|
+
return (_jsx("ol", { "aria-label": "Certificate steps", className: "flex flex-col gap-2", children: steps.map((step) => (_jsxs("li", { className: "flex items-start gap-2 text-sm", "aria-current": step.state === "active" ? "step" : undefined, children: [_jsx(StepIcon, { state: step.state }), _jsxs("span", { className: step.state === "pending" ? "text-text-muted" : "text-text", children: [step.label, _jsxs("span", { className: "sr-only", children: [" - ", STEP_TEXT[step.state]] })] }), step.at && _jsx("span", { className: "ml-auto shrink-0 text-xs text-text-muted", children: formatDateTime(step.at) })] }, step.id))) }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The progress bar: determinate when the server says how far along it is, an endless shimmer while a step is active and no number is known.
|
|
37
|
+
* `role="progressbar"` with `aria-valuenow` (left off when unknown, as ARIA says for an indeterminate bar) and a `aria-valuetext` in words.
|
|
38
|
+
*/
|
|
39
|
+
function ProgressBar({ value, text }) {
|
|
40
|
+
return (_jsx("div", { role: "progressbar", "aria-label": "Certificate progress", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": value, "aria-valuetext": text, className: "h-2 w-full overflow-hidden rounded-pill bg-border/60", children: value === undefined ? (_jsx("div", { className: "rr-progress-indeterminate h-full w-1/3 rounded-pill bg-primary motion-reduce:w-full motion-reduce:opacity-40" })) : (_jsx("div", { className: "rr-progress-shimmer h-full rounded-pill bg-primary transition-[width] duration-500", style: { width: `${Math.max(value, value > 0 ? 3 : 0)}%` } })) }));
|
|
41
|
+
}
|
|
42
|
+
function Detail({ label, children }) {
|
|
43
|
+
return (_jsxs("div", { className: "grid grid-cols-[6.5rem_1fr] gap-2 text-sm sm:grid-cols-[8rem_1fr]", children: [_jsx("dt", { className: "text-text-muted", children: label }), _jsx("dd", { className: "min-w-0 break-words", children: children })] }));
|
|
44
|
+
}
|
|
45
|
+
/** The sentence under the button: what the last manual check found, or why it cannot be pressed again yet. */
|
|
46
|
+
function checkMessage(snapshot, statusLine, now) {
|
|
47
|
+
if (snapshot?.offline) {
|
|
48
|
+
return "Could not reach the server - showing what was last known.";
|
|
49
|
+
}
|
|
50
|
+
const last = snapshot?.lastCheck;
|
|
51
|
+
if (!last) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (last.outcome === "failed") {
|
|
55
|
+
return "Could not check just now - try again in a moment.";
|
|
56
|
+
}
|
|
57
|
+
if (last.outcome === "limited") {
|
|
58
|
+
return "Checked a moment ago - please wait a few seconds before checking again.";
|
|
59
|
+
}
|
|
60
|
+
if (last.outcome === "changed") {
|
|
61
|
+
return `Updated - ${statusLine ?? "the status changed"}`;
|
|
62
|
+
}
|
|
63
|
+
return `Still waiting - checked ${relativeTime(last.at, now)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Settings > Encryption's "Digital signature certificate" card: where a signing-certificate enrollment is, in the words of the person waiting for
|
|
67
|
+
* it. A progress bar (determinate from the server's `progress`), the steps with a state icon and time each, a status line, when it was requested
|
|
68
|
+
* and last checked (kept current), and a "Check status" button with a busy state and a cooldown. An issued certificate shows its subject, issuer,
|
|
69
|
+
* serial (copyable) and expiry with a warning (and "Renew") inside 30 days; a failed one shows the reason and "Try again"; an expired one offers a
|
|
70
|
+
* new request. Everything but `mode` is optional so a server that only says `pending`/`issued`/`failed` still gets a useful card.
|
|
71
|
+
*/
|
|
72
|
+
export default function SigningCertificateCard({ mode, enrollment, snapshot, keyNotAfter, info, address, canRequest, requesting, onCheck, onRequest }) {
|
|
73
|
+
const busy = mode === "pending" || (snapshot?.retryAt ?? 0) > Date.now();
|
|
74
|
+
const now = useNow(busy ? 1000 : 30000);
|
|
75
|
+
const steps = stepsOf(enrollment);
|
|
76
|
+
const progress = progressOf(enrollment);
|
|
77
|
+
const statusLine = statusLineOf(enrollment);
|
|
78
|
+
// Any of the newer fields gives a percentage (the server's, or the share of steps done), so this is "the server told us more than pending".
|
|
79
|
+
const detailed = progress !== undefined;
|
|
80
|
+
const badge = BADGE[mode];
|
|
81
|
+
// What the words say depends on how this deployment issues certificates: a public CA does it by itself, an administrator uploads it, or nobody has said.
|
|
82
|
+
const issueMode = issueModeOf(enrollment, info);
|
|
83
|
+
const problem = healthWarning(info);
|
|
84
|
+
const notAfter = enrollment?.notAfter ?? keyNotAfter;
|
|
85
|
+
const expiry = expiryOf(notAfter, now);
|
|
86
|
+
const requestedAgo = relativeTime(enrollment?.requestedAt, now);
|
|
87
|
+
const checkedAt = timeOf(enrollment?.lastCheckedAt) ?? snapshot?.answeredAt ?? undefined;
|
|
88
|
+
const checkedAgo = relativeTime(checkedAt, now);
|
|
89
|
+
const waitSeconds = Math.max(0, Math.ceil(((snapshot?.retryAt ?? 0) - now) / 1000));
|
|
90
|
+
const checkingNow = snapshot?.checkingNow ?? false;
|
|
91
|
+
const message = checkMessage(snapshot, statusLine, now);
|
|
92
|
+
const overdue = isOverdue(enrollment, now);
|
|
93
|
+
return (_jsxs("section", { "aria-label": "Digital signature certificate", className: "flex flex-col gap-3 rounded-md border border-border bg-surface p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("h3", { className: "text-sm font-semibold", children: "Digital signature certificate" }), _jsx("span", { className: ["rounded-pill px-2 py-0.5 text-xs font-bold", badge.className].join(" "), children: badge.text })] }), mode === "pending" && (_jsxs(_Fragment, { children: [issueMode === "manual" ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm", children: MANUAL_ISSUE_TEXT }), info?.contactEmail && _jsxs("p", { className: "text-xs text-text-muted", children: ["Administrator: ", info.contactEmail] })] })) : (_jsxs(_Fragment, { children: [issueMode === "automatic" && _jsx("p", { className: "text-sm text-text-muted", children: requestedFromText(info, address) }), detailed ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm", children: statusLine ?? "In progress" }), _jsx(ProgressBar, { value: progress, text: `${progress} percent - ${statusLine ?? "in progress"}` })] })) : (_jsxs(_Fragment, { children: [_jsx(ProgressBar, { value: undefined, text: "In progress" }), issueMode === "unknown" && _jsx("p", { className: "text-sm text-text-muted", children: PENDING_FALLBACK_TEXT })] }))] })), problem && (_jsxs("div", { role: "alert", className: "flex items-start gap-2 rounded-sm border border-danger/40 bg-danger/10 px-3 py-2 text-sm", children: [_jsx(HiOutlineExclamationTriangle, { size: 18, "aria-hidden": "true", className: "mt-0.5 shrink-0 text-danger" }), _jsx("span", { children: problem })] })), overdue && (_jsxs("p", { className: "text-sm", children: ["This is taking longer than expected", checkedAgo ? ` - last checked ${checkedAgo}` : "", ". Use Check status to ask again."] })), enrollment?.errorCode === "ca-unreachable" && _jsx("p", { className: "text-sm text-text-muted", children: CA_UNREACHABLE_TEXT }), enrollment?.note && _jsx("p", { className: "text-xs text-text-muted", children: enrollment.note }), steps.length > 0 && _jsx(Stepper, { steps: steps }), issueMode === "automatic" && (_jsx("p", { className: "text-xs text-text-muted", children: "You can leave this page - you will be told when the certificate is issued or the request fails." })), (requestedAgo || checkedAgo) && (_jsxs("p", { className: "text-xs text-text-muted", children: [requestedAgo ? `Requested ${issueMode === "manual" ? formatDateTime(enrollment?.requestedAt) : requestedAgo}` : "", requestedAgo && checkedAgo ? " - " : "", checkedAgo ? `last checked ${checkedAgo}` : ""] })), onCheck && (_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: checkingNow, disabled: checkingNow || waitSeconds > 0, "aria-busy": checkingNow, onClick: onCheck, children: checkingNow ? "Checking..." : waitSeconds > 0 ? `Check again in ${waitSeconds} s` : "Check status" }), _jsx("span", { role: "status", "aria-live": "polite", className: "text-xs text-text-muted", children: message })] }))] })), mode === "issued" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm", children: isInstalling(enrollment)
|
|
94
|
+
? statusLineOf(enrollment)
|
|
95
|
+
: notAfter !== undefined
|
|
96
|
+
? `Issued - valid until ${formatDate(notAfter)}`
|
|
97
|
+
: "Issued" }), enrollment?.note && _jsx("p", { className: "text-xs text-text-muted", children: enrollment.note }), expiry.state === "soon" && (_jsxs("div", { role: "alert", className: "flex items-start gap-2 rounded-sm border border-danger/40 bg-danger/10 px-3 py-2 text-sm", children: [_jsx(HiOutlineExclamationTriangle, { size: 18, "aria-hidden": "true", className: "mt-0.5 shrink-0 text-danger" }), _jsxs("span", { children: ["This certificate expires ", expiry.daysLeft === 1 ? "tomorrow" : `in ${expiry.daysLeft} days`, ". Renew it so your mail stays signed."] })] })), (enrollment?.subject || enrollment?.issuer || enrollment?.serialNumber || enrollment?.issuedAt) && (_jsxs("dl", { className: "flex flex-col gap-1.5", children: [enrollment.subject && _jsx(Detail, { label: "Subject", children: enrollment.subject }), enrollment.issuer && _jsx(Detail, { label: "Issuer", children: enrollment.issuer }), enrollment.serialNumber && (_jsx(Detail, { label: "Serial number", children: _jsxs("span", { className: "inline-flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-mono text-xs", title: enrollment.serialNumber, children: truncateSerial(enrollment.serialNumber) }), _jsx(CopyButton, { value: enrollment.serialNumber, label: "Copy the certificate's serial number" })] }) })), enrollment.issuedAt && _jsx(Detail, { label: "Issued", children: formatDate(enrollment.issuedAt) }), notAfter !== undefined && _jsx(Detail, { label: "Valid until", children: formatDate(notAfter) })] })), canRequest && onRequest && expiry.state === "soon" && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: requesting, disabled: requesting, onClick: onRequest, children: "Renew" }) }))] })), mode === "failed" && (_jsxs(_Fragment, { children: [_jsx(Alert, { children: statusLine ?? "Failed: the certificate could not be issued." }), steps.length > 0 && _jsx(Stepper, { steps: steps }), canRequest && onRequest && enrollment?.retryable !== false && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: requesting, disabled: requesting, onClick: onRequest, children: "Try again" }) }))] })), mode === "expired" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm", children: notAfter !== undefined ? `Expired on ${formatDate(notAfter)}. Mail from this mailbox is no longer signed.` : "This certificate has expired. Mail from this mailbox is no longer signed." }), canRequest && onRequest && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: requesting, disabled: requesting, onClick: onRequest, children: "Request a new certificate" }) }))] }))] }));
|
|
98
|
+
}
|
|
@@ -38,4 +38,4 @@ export declare function useSettingsShell(): SettingsShellContextValue;
|
|
|
38
38
|
* settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
|
|
39
39
|
* app rail, since this framework has no client-side router.
|
|
40
40
|
*/
|
|
41
|
-
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<SettingsShellProps>): React.JSX.Element;
|
|
41
|
+
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<SettingsShellProps>): React.JSX.Element;
|
|
@@ -11,10 +11,12 @@ import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
|
11
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
12
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
|
+
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
14
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
16
|
import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
|
|
16
17
|
/** Every settings section with its own sidebar entry. */
|
|
17
18
|
export const SETTINGS_SECTIONS = [
|
|
19
|
+
{ id: "appearance", href: "/settings/appearance", label: "Appearance" },
|
|
18
20
|
{ id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
|
|
19
21
|
{ id: "filters", href: "/settings/filters", label: "Mail Filters" },
|
|
20
22
|
{ id: "signatures", href: "/settings/signatures", label: "Signatures" },
|
|
@@ -46,15 +48,24 @@ export function useSettingsShell() {
|
|
|
46
48
|
* settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
|
|
47
49
|
* app rail, since this framework has no client-side router.
|
|
48
50
|
*/
|
|
49
|
-
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
51
|
+
export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
50
52
|
const [status, setStatus] = useState("checking");
|
|
51
53
|
const [error, setError] = useState(null);
|
|
52
54
|
const [mailboxes, setMailboxes] = useState([]);
|
|
53
55
|
const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
|
|
54
56
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
57
|
+
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
58
|
+
// effect, not during render, so the server render and the hydrating render agree.
|
|
59
|
+
const search = useLocationSearch();
|
|
60
|
+
const navigate = useNavigate();
|
|
55
61
|
useEffect(() => {
|
|
56
|
-
setRequestedMailboxUid(new URLSearchParams(
|
|
57
|
-
}, []);
|
|
62
|
+
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
63
|
+
}, [search]);
|
|
64
|
+
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
65
|
+
// with the page - is closed here.
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setDrawerOpen(false);
|
|
68
|
+
}, [search]);
|
|
58
69
|
useEffect(() => {
|
|
59
70
|
if (!userUid) {
|
|
60
71
|
return;
|
|
@@ -94,7 +105,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
94
105
|
// `SettingsShellProps.active`), but a plugin page's section is missing when the
|
|
95
106
|
// server didn't send its nav item, so that falls back to the current path.
|
|
96
107
|
const href = sections.find((s) => s.id === active)?.href ?? window.location.pathname;
|
|
97
|
-
|
|
108
|
+
navigate(`${href}?mailboxUid=${encodeURIComponent(e.target.value)}`);
|
|
98
109
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
|
|
99
110
|
// `inner` (this whole branch) is only ever computed once `status === "ready"`
|
|
100
111
|
// and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
|
|
@@ -109,5 +120,5 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
109
120
|
].join(" "), children: section.label }, section.id))) })] }));
|
|
110
121
|
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
111
122
|
}
|
|
112
|
-
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
|
|
123
|
+
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
113
124
|
}
|