@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,199 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
import { HiOutlineEllipsisHorizontal } from "react-icons/hi2";
|
|
9
|
+
/** Room (px) each kind of button takes, and what the bar spends on its padding, a divider and the "More" button - the layout is computed from them
|
|
10
|
+
* rather than measured, so the first paint on the client is already right and nothing jumps. */
|
|
11
|
+
export const LABELLED_BUTTON_WIDTH = 64;
|
|
12
|
+
export const ICON_BUTTON_WIDTH = 36;
|
|
13
|
+
const BAR_PADDING = 16;
|
|
14
|
+
const DIVIDER_WIDTH = 9;
|
|
15
|
+
function widthOf(actions, perButton, more) {
|
|
16
|
+
const groups = new Set(actions.map((action) => action.group)).size;
|
|
17
|
+
return BAR_PADDING + actions.length * perButton + Math.max(0, groups - 1) * DIVIDER_WIDTH + (more ? perButton : 0);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Which buttons a bar of `width` px shows, and how: every one with its caption when that fits; else every one as an icon (the captions go first);
|
|
21
|
+
* else as many icons as fit, the least important (`rank`) going into a "More" menu one at a time - the essential ones stay whatever the width.
|
|
22
|
+
* Pure, so it is the same on the server and the client for a given width; `width === undefined` (not measured yet, no ResizeObserver) shows
|
|
23
|
+
* everything.
|
|
24
|
+
*/
|
|
25
|
+
export function layoutToolbar(actions, width) {
|
|
26
|
+
const ordered = [...actions].sort((a, b) => a.group - b.group);
|
|
27
|
+
const all = ordered.map((action) => action.id);
|
|
28
|
+
if (width === undefined) {
|
|
29
|
+
return { labels: true, visible: all };
|
|
30
|
+
}
|
|
31
|
+
if (widthOf(ordered, LABELLED_BUTTON_WIDTH, false) <= width) {
|
|
32
|
+
return { labels: true, visible: all };
|
|
33
|
+
}
|
|
34
|
+
if (widthOf(ordered, ICON_BUTTON_WIDTH, false) <= width) {
|
|
35
|
+
return { labels: false, visible: all };
|
|
36
|
+
}
|
|
37
|
+
// Not everything fits: the least important actions go into "More" one at a time, until what is left and the "More" button do.
|
|
38
|
+
let shown = ordered;
|
|
39
|
+
for (const dropped of ordered.filter((action) => !action.essential).sort((x, y) => x.rank - y.rank)) {
|
|
40
|
+
if (widthOf(shown, ICON_BUTTON_WIDTH, true) <= width) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
shown = shown.filter((action) => action !== dropped);
|
|
44
|
+
}
|
|
45
|
+
return { labels: false, visible: shown.map((action) => action.id) };
|
|
46
|
+
}
|
|
47
|
+
/** The width of `ref`'s element, kept current with a ResizeObserver; `undefined` until measured (and where there is no ResizeObserver). */
|
|
48
|
+
export function useElementWidth(ref) {
|
|
49
|
+
const [width, setWidth] = useState(undefined);
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
const element = ref.current;
|
|
52
|
+
if (!element || typeof ResizeObserver === "undefined") {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setWidth(element.getBoundingClientRect().width);
|
|
56
|
+
const observer = new ResizeObserver((entries) => {
|
|
57
|
+
const box = entries[0]?.contentRect.width;
|
|
58
|
+
if (box !== undefined) {
|
|
59
|
+
setWidth(Math.round(box));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
observer.observe(element);
|
|
63
|
+
return () => observer.disconnect();
|
|
64
|
+
}, [ref]);
|
|
65
|
+
return width;
|
|
66
|
+
}
|
|
67
|
+
function ToolbarButton({ action, labels }) {
|
|
68
|
+
const Icon = action.icon;
|
|
69
|
+
return (_jsxs("button", { type: "button", onClick: action.onClick, disabled: action.disabled, "aria-pressed": action.pressed,
|
|
70
|
+
// A tooltip when the caption is not on show (the shortcut's own title, when it has one, wins).
|
|
71
|
+
title: action.hint?.title ?? (labels ? undefined : action.label), "aria-keyshortcuts": action.hint?.["aria-keyshortcuts"], "data-toolbar-item": "", className: [
|
|
72
|
+
"flex shrink-0 flex-col items-center justify-center gap-1 rounded-sm text-xs disabled:opacity-40 disabled:cursor-not-allowed",
|
|
73
|
+
labels ? "w-16 py-1.5" : "h-9 w-9",
|
|
74
|
+
action.pressed ? "bg-primary/10 text-primary-dark" : "text-text-muted hover:not-disabled:bg-surface-alt hover:not-disabled:text-text",
|
|
75
|
+
].join(" "), children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { className: labels ? "leading-tight text-center" : "sr-only", children: action.label })] }));
|
|
76
|
+
}
|
|
77
|
+
/** The menu that holds what does not fit: a button, and a portalled `role="menu"` so nothing clips it. Arrow keys, Home, End and Escape; focus returns to the button. */
|
|
78
|
+
function MoreMenu({ actions }) {
|
|
79
|
+
const [open, setOpen] = useState(false);
|
|
80
|
+
const [anchor, setAnchor] = useState(null);
|
|
81
|
+
const buttonRef = useRef(null);
|
|
82
|
+
const menuRef = useRef(null);
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
if (!open) {
|
|
85
|
+
setAnchor(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
function place() {
|
|
89
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
90
|
+
setAnchor({ right: Math.max(8, window.innerWidth - rect.right), top: rect.bottom + 4 });
|
|
91
|
+
}
|
|
92
|
+
place();
|
|
93
|
+
window.addEventListener("resize", place);
|
|
94
|
+
window.addEventListener("scroll", place, true);
|
|
95
|
+
return () => {
|
|
96
|
+
window.removeEventListener("resize", place);
|
|
97
|
+
window.removeEventListener("scroll", place, true);
|
|
98
|
+
};
|
|
99
|
+
}, [open]);
|
|
100
|
+
// On open, the first item that can be used gets the focus.
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (open && anchor) {
|
|
103
|
+
menuRef.current?.querySelector('[role="menuitem"]:not(:disabled)')?.focus();
|
|
104
|
+
}
|
|
105
|
+
}, [open, anchor !== null]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!open) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
function handlePointerDown(event) {
|
|
111
|
+
if (event.target instanceof Node && !buttonRef.current?.contains(event.target) && !menuRef.current?.contains(event.target)) {
|
|
112
|
+
setOpen(false);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
116
|
+
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
117
|
+
}, [open]);
|
|
118
|
+
function close(refocus) {
|
|
119
|
+
setOpen(false);
|
|
120
|
+
if (refocus) {
|
|
121
|
+
buttonRef.current?.focus();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function handleMenuKeyDown(event) {
|
|
125
|
+
const items = Array.from(menuRef.current.querySelectorAll('[role="menuitem"]:not(:disabled)'));
|
|
126
|
+
const index = items.indexOf(document.activeElement);
|
|
127
|
+
const focusAt = (next) => {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
items[(next + items.length) % items.length]?.focus();
|
|
130
|
+
};
|
|
131
|
+
if (event.key === "Escape") {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
close(true);
|
|
135
|
+
}
|
|
136
|
+
else if (event.key === "ArrowDown") {
|
|
137
|
+
focusAt(index + 1);
|
|
138
|
+
}
|
|
139
|
+
else if (event.key === "ArrowUp") {
|
|
140
|
+
focusAt(index - 1);
|
|
141
|
+
}
|
|
142
|
+
else if (event.key === "Home") {
|
|
143
|
+
focusAt(0);
|
|
144
|
+
}
|
|
145
|
+
else if (event.key === "End") {
|
|
146
|
+
focusAt(items.length - 1);
|
|
147
|
+
}
|
|
148
|
+
else if (event.key === "Tab") {
|
|
149
|
+
close(false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: buttonRef, type: "button", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": "More actions", title: "More actions", "data-toolbar-item": "", onClick: () => setOpen((current) => !current), onKeyDown: (event) => {
|
|
153
|
+
if (event.key === "ArrowDown" && !open) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
setOpen(true);
|
|
156
|
+
}
|
|
157
|
+
}, className: [
|
|
158
|
+
"flex h-9 w-9 shrink-0 items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text",
|
|
159
|
+
open ? "bg-surface-alt text-text" : "",
|
|
160
|
+
].join(" "), children: _jsx(HiOutlineEllipsisHorizontal, { size: 18, "aria-hidden": "true" }) }), open &&
|
|
161
|
+
anchor &&
|
|
162
|
+
createPortal(_jsx("div", { ref: menuRef, role: "menu", "aria-label": "More actions", onKeyDown: handleMenuKeyDown, style: { position: "fixed", right: anchor.right, top: anchor.top }, className: "z-[70] w-52 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-surface py-1.5 shadow-modal", children: actions.map((action) => {
|
|
163
|
+
const Icon = action.icon;
|
|
164
|
+
return (_jsxs("button", { type: "button", role: "menuitem", disabled: action.disabled, title: action.hint?.title, "aria-keyshortcuts": action.hint?.["aria-keyshortcuts"], onClick: () => {
|
|
165
|
+
close(true);
|
|
166
|
+
action.onClick();
|
|
167
|
+
}, className: "flex w-full items-center gap-3 px-3.5 py-2 text-left text-sm text-text hover:not-disabled:bg-surface-alt focus-visible:bg-surface-alt disabled:cursor-not-allowed disabled:opacity-40", children: [_jsx(Icon, { size: 18, "aria-hidden": "true", className: "shrink-0 text-text-muted" }), action.label] }, action.id));
|
|
168
|
+
}) }), document.body)] }));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A toolbar that fits the column it is in - never wider, never over another pane. It measures its own width (`ResizeObserver`) and shows every
|
|
172
|
+
* action with a caption when there is room; when there is not, the captions go first (each button keeps its tooltip and accessible name), then the
|
|
173
|
+
* least important actions move into a "More" menu (`layoutToolbar()`). Arrow keys, Home and End move between the buttons; the menu is a real
|
|
174
|
+
* `role="menu"`. `children` (a hidden file input, say) are rendered inside the toolbar's element but take no room.
|
|
175
|
+
*/
|
|
176
|
+
export default function ResponsiveToolbar({ label, actions, leading, children }) {
|
|
177
|
+
const ref = useRef(null);
|
|
178
|
+
const width = useElementWidth(ref);
|
|
179
|
+
const layout = layoutToolbar(actions, width);
|
|
180
|
+
const ordered = [...actions].sort((a, b) => a.group - b.group);
|
|
181
|
+
const shown = ordered.filter((action) => layout.visible.includes(action.id));
|
|
182
|
+
const hidden = ordered.filter((action) => !layout.visible.includes(action.id));
|
|
183
|
+
function handleKeyDown(event) {
|
|
184
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key) || event.target.closest('[role="menu"]')) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const items = Array.from(ref.current.querySelectorAll("[data-toolbar-item]:not(:disabled)"));
|
|
188
|
+
const index = items.indexOf(event.target);
|
|
189
|
+
if (index === -1) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : index + (event.key === "ArrowRight" ? 1 : -1);
|
|
194
|
+
items[(next + items.length) % items.length].focus();
|
|
195
|
+
}
|
|
196
|
+
return (_jsxs("div", { ref: ref, role: "toolbar", "aria-label": label, onKeyDown: handleKeyDown,
|
|
197
|
+
// `overflow-x-clip` is the guarantee: whatever the layout says, the bar can never paint outside its own column.
|
|
198
|
+
className: "flex min-w-0 items-stretch gap-0.5 overflow-x-clip border-b border-border bg-surface-alt px-2 py-1", children: [leading, shown.map((action, index) => (_jsxs(React.Fragment, { children: [index > 0 && shown[index - 1].group !== action.group && _jsx("div", { className: "my-1 w-px shrink-0 self-stretch bg-border", "aria-hidden": "true" }), _jsx(ToolbarButton, { action: action, labels: layout.labels })] }, action.id))), hidden.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 w-px shrink-0 self-stretch bg-border", "aria-hidden": "true" }), _jsx(MoreMenu, { actions: hidden })] })), children] }));
|
|
199
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The account menu's colour scheme control: a row "Theme" with a three-way segmented control of icon buttons - System, Light, Dark - a
|
|
4
|
+
* `radiogroup` (one tab stop, the arrow keys move and choose, Home/End jump). A choice applies through `useAppearance().setPrefs()`, which
|
|
5
|
+
* changes every open surface in the same frame (the app frame, message cards, the consoles) and saves in the background - and puts the change
|
|
6
|
+
* back, with a notice, if the save fails. Renders nothing where there is no `AppearanceProvider` (a shell that has none, a component tested
|
|
7
|
+
* alone): the hook is inert there and a control that changes nothing would be a lie.
|
|
8
|
+
*/
|
|
9
|
+
export default function ThemeSwitch(): React.JSX.Element | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useContext, useId, useRef } from "react";
|
|
7
|
+
import { HiOutlineComputerDesktop, HiOutlineMoon, HiOutlineSun } from "react-icons/hi2";
|
|
8
|
+
import { AppearanceContext, INERT_APPEARANCE } from "../../appearance/appearanceContext.js";
|
|
9
|
+
const CHOICES = [
|
|
10
|
+
{ mode: "system", label: "System", hint: "System - follow this device", Icon: HiOutlineComputerDesktop },
|
|
11
|
+
{ mode: "light", label: "Light", hint: "Light", Icon: HiOutlineSun },
|
|
12
|
+
{ mode: "dark", label: "Dark", hint: "Dark", Icon: HiOutlineMoon },
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* The account menu's colour scheme control: a row "Theme" with a three-way segmented control of icon buttons - System, Light, Dark - a
|
|
16
|
+
* `radiogroup` (one tab stop, the arrow keys move and choose, Home/End jump). A choice applies through `useAppearance().setPrefs()`, which
|
|
17
|
+
* changes every open surface in the same frame (the app frame, message cards, the consoles) and saves in the background - and puts the change
|
|
18
|
+
* back, with a notice, if the save fails. Renders nothing where there is no `AppearanceProvider` (a shell that has none, a component tested
|
|
19
|
+
* alone): the hook is inert there and a control that changes nothing would be a lie.
|
|
20
|
+
*/
|
|
21
|
+
export default function ThemeSwitch() {
|
|
22
|
+
const appearance = useContext(AppearanceContext);
|
|
23
|
+
const buttons = useRef([]);
|
|
24
|
+
const labelId = useId();
|
|
25
|
+
if (appearance === INERT_APPEARANCE) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const current = appearance.prefs.mode;
|
|
29
|
+
function choose(index, focus) {
|
|
30
|
+
const next = CHOICES[(index + CHOICES.length) % CHOICES.length];
|
|
31
|
+
if (next.mode !== current) {
|
|
32
|
+
appearance.setPrefs({ mode: next.mode });
|
|
33
|
+
}
|
|
34
|
+
if (focus) {
|
|
35
|
+
buttons.current[(index + CHOICES.length) % CHOICES.length]?.focus();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function handleKeyDown(event, index) {
|
|
39
|
+
const target = { ArrowRight: index + 1, ArrowDown: index + 1, ArrowLeft: index - 1, ArrowUp: index - 1, Home: 0, End: CHOICES.length - 1 }[event.key];
|
|
40
|
+
if (target !== undefined) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
choose(target, true);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3.5 py-2 border-b border-border", children: [_jsx("span", { id: labelId, className: "text-sm text-text", children: "Theme" }), _jsx("div", { role: "radiogroup", "aria-labelledby": labelId, className: "inline-flex rounded-sm border border-border bg-surface-alt p-0.5", children: CHOICES.map(({ mode, label, hint, Icon }, index) => {
|
|
46
|
+
const selected = mode === current;
|
|
47
|
+
return (_jsx("button", { ref: (element) => {
|
|
48
|
+
buttons.current[index] = element;
|
|
49
|
+
}, type: "button", role: "radio", "aria-checked": selected, "aria-label": label, title: hint, tabIndex: selected ? 0 : -1, onClick: () => choose(index, false), onKeyDown: (event) => handleKeyDown(event, index), className: [
|
|
50
|
+
"flex h-7 w-8 items-center justify-center rounded-[3px] focus-visible:outline-2 focus-visible:outline-primary",
|
|
51
|
+
selected ? "bg-surface text-primary-dark shadow-sm" : "text-text-muted hover:text-text",
|
|
52
|
+
].join(" "), children: _jsx(Icon, { size: 16, "aria-hidden": "true" }) }, mode));
|
|
53
|
+
}) })] }));
|
|
54
|
+
}
|
|
@@ -5,22 +5,44 @@ export interface UserMenuProps {
|
|
|
5
5
|
* the "Account" item links into, if configured. Without it the menu shows the bare uid and has no "Account" item. */
|
|
6
6
|
authServerUrl?: string;
|
|
7
7
|
onSignOut: () => void;
|
|
8
|
-
/** Shows an "Admin" item linking to `/admin`, above "Sign Out" — pass only for a trusted-role caller
|
|
8
|
+
/** Shows an "Admin Console" item linking to `/admin`, above "Sign Out" — pass only for a trusted-role caller
|
|
9
|
+
* (the JWT carries the role, i.e. an elevated session). See `detectAdmin` for everyone else. */
|
|
9
10
|
showAdminLink?: boolean;
|
|
11
|
+
/** Asks auth-server whether the caller holds a trusted role (`GET /api/users/me`, once per sitting - see
|
|
12
|
+
* `lookUpAdminAccess()`) and shows the "Admin Console" item if so. Needed because a token that isn't elevated
|
|
13
|
+
* carries no trusted role, so an administrator with an ordinary session is not `showAdminLink`. Does nothing
|
|
14
|
+
* without `authServerUrl`, and a failed lookup leaves the item hidden. Pass `false` while impersonating. */
|
|
15
|
+
detectAdmin?: boolean;
|
|
16
|
+
/** The role names the server treats as trusted (its `trusted_roles` config) - what `detectAdmin` looks for.
|
|
17
|
+
* Defaults to `["admin"]`, the server's own default. */
|
|
18
|
+
trustedRoles?: readonly string[];
|
|
10
19
|
/** Shows a "Settings" item, above "Admin"/"Sign Out" — an account-scoped surface (like "Admin"), not
|
|
11
20
|
* a content-scoped one, so it lives here rather than as a 5th `AppShell` rail icon. Links straight to
|
|
12
21
|
* `/settings/auto-reply`, the only settings section that exists today; repoint this at a real
|
|
13
22
|
* `/settings` landing page once a second section (Mail Filters, Signatures) makes one worth building. */
|
|
14
23
|
showSettingsLink?: boolean;
|
|
24
|
+
/** Shows the new-mail notification controls, above "Admin Console"/"Sign Out": a "New mail pop-ups" on/off switch and, while
|
|
25
|
+
* the browser hasn't been asked, "Turn on desktop notifications". Both are per browser (see `newMailNotifications.ts`).
|
|
26
|
+
* For the shells that announce new mail (Mail); the consoles have nothing to announce. */
|
|
27
|
+
showNotificationSettings?: boolean;
|
|
28
|
+
/** Shows a "Keyboard shortcuts" item that calls this - opening the help dialog (`?` and Ctrl+/ do too). For the shells that have the
|
|
29
|
+
* keyboard layer (`AppChrome`); the consoles don't offer it. */
|
|
30
|
+
onShowShortcuts?: () => void;
|
|
31
|
+
/** Which way the menu opens from its button: `"down"` (the default; in a header) or `"up"` (in a footer, where there is no room below). */
|
|
32
|
+
placement?: "down" | "up";
|
|
33
|
+
/** Shows a "Recent notifications" item that calls this - opening the history of the last pop-ups (`AppChrome` only). */
|
|
34
|
+
onShowNotifications?: () => void;
|
|
35
|
+
/** Errors in that history nobody has looked at yet: a count beside the item, and a dot on the menu button. */
|
|
36
|
+
unseenErrors?: number;
|
|
15
37
|
}
|
|
16
38
|
/**
|
|
17
39
|
* The top-right "who am I" menu shared by every shell (`AppShell`, `AdminShell`, `EscrowShell`): an avatar-button
|
|
18
40
|
* trigger that opens a dropdown showing the caller's avatar/name, an "Account" link to auth-server's account page,
|
|
19
|
-
* an optional "Settings" and "Admin" link, and "Sign Out". This service has no local user directory (see
|
|
41
|
+
* an optional "Settings" and "Admin Console" link, and "Sign Out". This service has no local user directory (see
|
|
20
42
|
* `.claude/NOTES.md`), so the name and avatar come from auth-server, and the displayed name (and the initials badge)
|
|
21
43
|
* falls back down a chain: the profile's name (`GET /api/profiles/me`), else the caller's username - their first
|
|
22
44
|
* verified `name` alias (`GET /api/aliases?type=name`, asked for only when the profile gave no name) - else the bare
|
|
23
45
|
* uid. Either lookup can fail (auth-server's CORS list not including this origin, or no profile document at all - a
|
|
24
46
|
* 404 for some accounts) and neither ever surfaces as an error. The avatar image comes from the profile alone.
|
|
25
47
|
*/
|
|
26
|
-
export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, showSettingsLink }: UserMenuProps): React.JSX.Element;
|
|
48
|
+
export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, detectAdmin, trustedRoles, showSettingsLink, showNotificationSettings, onShowShortcuts, placement, onShowNotifications, unseenErrors, }: UserMenuProps): React.JSX.Element;
|
|
@@ -3,8 +3,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
import { HiOutlineShieldCheck } from "react-icons/hi2";
|
|
7
9
|
import { formatProfileName, getMyProfile, getMyUsername, profileInitials } from "@rapidmx/react-shared/auth/profileApi.js";
|
|
10
|
+
import { accountUrlOf } from "../../auth/accountUrl.js";
|
|
11
|
+
import { DEFAULT_TRUSTED_ROLES, lookUpAdminAccess } from "../../auth/adminAccess.js";
|
|
12
|
+
import { ariaKeyShortcuts } from "../../keyboard/format.js";
|
|
13
|
+
import { SHORTCUTS } from "../../keyboard/keymap.js";
|
|
14
|
+
import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
|
|
15
|
+
import { SETTINGS_HREF } from "../../navigation/appHrefs.js";
|
|
16
|
+
import ThemeSwitch from "./ThemeSwitch.js";
|
|
17
|
+
import { desktopPermission, getNewMailPopupsEnabled, requestDesktopPermission, setDesktopOfferDismissed, setNewMailPopupsEnabled, } from "../../mail/newMailNotifications.js";
|
|
8
18
|
function Avatar({ profile, initials, large }) {
|
|
9
19
|
const sizeClass = large ? "w-10 h-10 text-base" : "w-8 h-8 text-sm";
|
|
10
20
|
if (profile?.avatar) {
|
|
@@ -15,15 +25,23 @@ function Avatar({ profile, initials, large }) {
|
|
|
15
25
|
/**
|
|
16
26
|
* The top-right "who am I" menu shared by every shell (`AppShell`, `AdminShell`, `EscrowShell`): an avatar-button
|
|
17
27
|
* trigger that opens a dropdown showing the caller's avatar/name, an "Account" link to auth-server's account page,
|
|
18
|
-
* an optional "Settings" and "Admin" link, and "Sign Out". This service has no local user directory (see
|
|
28
|
+
* an optional "Settings" and "Admin Console" link, and "Sign Out". This service has no local user directory (see
|
|
19
29
|
* `.claude/NOTES.md`), so the name and avatar come from auth-server, and the displayed name (and the initials badge)
|
|
20
30
|
* falls back down a chain: the profile's name (`GET /api/profiles/me`), else the caller's username - their first
|
|
21
31
|
* verified `name` alias (`GET /api/aliases?type=name`, asked for only when the profile gave no name) - else the bare
|
|
22
32
|
* uid. Either lookup can fail (auth-server's CORS list not including this origin, or no profile document at all - a
|
|
23
33
|
* 404 for some accounts) and neither ever surfaces as an error. The avatar image comes from the profile alone.
|
|
24
34
|
*/
|
|
25
|
-
export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, showSettingsLink }) {
|
|
35
|
+
export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, detectAdmin, trustedRoles = DEFAULT_TRUSTED_ROLES, showSettingsLink, showNotificationSettings, onShowShortcuts, placement = "down", onShowNotifications, unseenErrors = 0, }) {
|
|
36
|
+
const env = useKeyEnvironment();
|
|
26
37
|
const [open, setOpen] = useState(false);
|
|
38
|
+
// Where the menu sits, in window coordinates: it is drawn through a portal into `<body>` with `position: fixed`, so no ancestor's
|
|
39
|
+
// `overflow`, `z-index` or stacking context - a custom branding header's, say - can clip it or put it behind the page.
|
|
40
|
+
const [anchor, setAnchor] = useState(null);
|
|
41
|
+
const menuRef = useRef(null);
|
|
42
|
+
const [detectedAdmin, setDetectedAdmin] = useState(false);
|
|
43
|
+
const [popups, setPopups] = useState(true);
|
|
44
|
+
const [permission, setPermission] = useState("unsupported");
|
|
27
45
|
const [profile, setProfile] = useState(undefined);
|
|
28
46
|
const [username, setUsername] = useState(undefined);
|
|
29
47
|
const containerRef = useRef(null);
|
|
@@ -56,12 +74,49 @@ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminL
|
|
|
56
74
|
cancelled = true;
|
|
57
75
|
};
|
|
58
76
|
}, [authServerUrl]);
|
|
77
|
+
// Keyed on the joined names, not the array: a caller's fresh `["admin"]` each render must not re-ask.
|
|
78
|
+
const trustedRolesKey = JSON.stringify(trustedRoles);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!detectAdmin || !authServerUrl || showAdminLink) {
|
|
81
|
+
setDetectedAdmin(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
void lookUpAdminAccess(authServerUrl, userUid, JSON.parse(trustedRolesKey)).then((admin) => {
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setDetectedAdmin(admin === true);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true;
|
|
92
|
+
};
|
|
93
|
+
}, [detectAdmin, authServerUrl, userUid, showAdminLink, trustedRolesKey]);
|
|
94
|
+
// Read fresh each time the menu opens - another tab may have changed either since - and only on the client.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (open && showNotificationSettings) {
|
|
97
|
+
setPopups(getNewMailPopupsEnabled());
|
|
98
|
+
setPermission(desktopPermission());
|
|
99
|
+
}
|
|
100
|
+
}, [open, showNotificationSettings]);
|
|
101
|
+
function togglePopups() {
|
|
102
|
+
setNewMailPopupsEnabled(!popups);
|
|
103
|
+
setPopups(!popups);
|
|
104
|
+
}
|
|
105
|
+
async function enableDesktopNotifications() {
|
|
106
|
+
// Asked from this click, as browsers require; a "no" (or "yes") answers the first pop-up's offer as well.
|
|
107
|
+
setPermission(await requestDesktopPermission());
|
|
108
|
+
setDesktopOfferDismissed(true);
|
|
109
|
+
}
|
|
59
110
|
useEffect(() => {
|
|
60
111
|
if (!open) {
|
|
61
112
|
return;
|
|
62
113
|
}
|
|
63
114
|
function handlePointerDown(e) {
|
|
64
|
-
|
|
115
|
+
// The menu is portalled out of the container, so it and the button are checked separately.
|
|
116
|
+
if (containerRef.current &&
|
|
117
|
+
e.target instanceof Node &&
|
|
118
|
+
!containerRef.current.contains(e.target) &&
|
|
119
|
+
!menuRef.current?.contains(e.target)) {
|
|
65
120
|
setOpen(false);
|
|
66
121
|
}
|
|
67
122
|
}
|
|
@@ -77,9 +132,35 @@ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminL
|
|
|
77
132
|
document.removeEventListener("keydown", handleKeyDown);
|
|
78
133
|
};
|
|
79
134
|
}, [open]);
|
|
135
|
+
// Measured before paint, and again whenever the window resizes or anything scrolls (a footer's button moves with the page).
|
|
136
|
+
useLayoutEffect(() => {
|
|
137
|
+
if (!open) {
|
|
138
|
+
setAnchor(null);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
function place() {
|
|
142
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
143
|
+
const right = Math.max(8, window.innerWidth - rect.right);
|
|
144
|
+
setAnchor(placement === "up" ? { right, bottom: window.innerHeight - rect.top + 8 } : { right, top: rect.bottom + 8 });
|
|
145
|
+
}
|
|
146
|
+
place();
|
|
147
|
+
window.addEventListener("resize", place);
|
|
148
|
+
window.addEventListener("scroll", place, true);
|
|
149
|
+
return () => {
|
|
150
|
+
window.removeEventListener("resize", place);
|
|
151
|
+
window.removeEventListener("scroll", place, true);
|
|
152
|
+
};
|
|
153
|
+
}, [open, placement]);
|
|
80
154
|
const name = formatProfileName(profile) ?? username ?? userUid;
|
|
81
155
|
const initials = profileInitials(profile, userUid, username);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
156
|
+
const accountUrl = accountUrlOf(authServerUrl);
|
|
157
|
+
return (_jsxs("div", { className: "relative", ref: containerRef, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "true", "aria-expanded": open, "aria-label": "Account menu", className: "relative flex items-center gap-1.5 rounded-pill py-1 pl-1 pr-2 hover:bg-surface-alt", children: [_jsx(Avatar, { profile: profile, initials: initials }), unseenErrors > 0 && (_jsx("span", { "data-testid": "unseen-errors-dot", "aria-hidden": "true", className: "absolute right-0.5 top-0.5 h-2.5 w-2.5 rounded-full border-2 border-surface bg-danger" })), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "none", className: `text-text-muted transition-transform ${open ? "rotate-180" : ""}`, "aria-hidden": "true", children: _jsx("path", { d: "M5 7.5L10 12.5L15 7.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), open &&
|
|
158
|
+
anchor &&
|
|
159
|
+
createPortal(_jsxs("div", { ref: menuRef, role: "menu", style: { position: "fixed", right: anchor.right, top: anchor.top, bottom: anchor.bottom }, className: "w-60 max-w-[calc(100vw-1rem)] max-h-[calc(100vh-5rem)] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1.5 z-[70]", children: [_jsxs("div", { className: "flex items-center gap-3 px-3.5 py-2.5 border-b border-border", children: [_jsx(Avatar, { profile: profile, initials: initials, large: true }), _jsx("span", { className: "text-sm font-semibold text-text truncate", children: name })] }), _jsx(ThemeSwitch, {}), accountUrl && (_jsx("a", { role: "menuitem", href: accountUrl, className: "block px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Account" })), showSettingsLink && (_jsx("a", { role: "menuitem", href: SETTINGS_HREF, className: "block px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Settings" })), showNotificationSettings && (_jsxs("button", { role: "menuitemcheckbox", "aria-checked": popups, type: "button", onClick: togglePopups, className: "flex w-full items-center justify-between gap-2 px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: [_jsx("span", { children: "New mail pop-ups" }), _jsx("span", { className: "text-xs font-semibold text-text-muted", children: popups ? "On" : "Off" })] })), showNotificationSettings && permission === "default" && (_jsx("button", { role: "menuitem", type: "button", onClick: () => void enableDesktopNotifications(), className: "block w-full px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: "Turn on desktop notifications" })), onShowNotifications && (_jsxs("button", { role: "menuitem", type: "button", onClick: () => {
|
|
160
|
+
setOpen(false);
|
|
161
|
+
onShowNotifications();
|
|
162
|
+
}, className: "flex w-full items-center justify-between gap-2 px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: [_jsx("span", { children: "Recent notifications" }), unseenErrors > 0 && (_jsxs("span", { className: "rounded-pill bg-danger px-1.5 text-xs font-bold text-white", children: [_jsx("span", { "aria-hidden": "true", children: unseenErrors }), _jsxs("span", { className: "sr-only", children: [unseenErrors, " unseen ", unseenErrors === 1 ? "error" : "errors"] })] }))] })), onShowShortcuts && (_jsx("button", { role: "menuitem", type: "button", "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.global.help, env), onClick: () => {
|
|
163
|
+
setOpen(false);
|
|
164
|
+
onShowShortcuts();
|
|
165
|
+
}, className: "block w-full px-3.5 py-2 text-left text-sm text-text hover:bg-surface-alt", children: "Keyboard shortcuts" })), (showAdminLink || detectedAdmin) && (_jsxs("a", { role: "menuitem", href: "/admin", className: "flex items-center gap-2 px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: [_jsx(HiOutlineShieldCheck, { size: 16, "aria-hidden": "true", className: "shrink-0 text-text-muted" }), "Admin Console"] })), _jsx("button", { role: "menuitem", type: "button", onClick: onSignOut, className: "block w-full text-left px-3.5 py-2 text-sm text-text hover:bg-surface-alt", children: "Sign Out" })] }), document.body)] }));
|
|
85
166
|
}
|
|
@@ -8,7 +8,9 @@ import { HiChevronDown, HiChevronRight, HiOutlineFlag, HiOutlinePaperClip } from
|
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
10
10
|
import MailAddress from "./MailAddress.js";
|
|
11
|
+
import { EncryptedPreview, conversationLooksEncrypted } from "./reading/EncryptedPreview.js";
|
|
11
12
|
import { listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
|
|
13
|
+
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
|
|
12
14
|
/** Every participant as `Name <address>`, for the tooltip and the accessible text of the "+N" a crowded row folds them into. */
|
|
13
15
|
function participantList(conversation) {
|
|
14
16
|
return conversation.participants.map((participant) => formatMailAddress(participant)).join(", ");
|
|
@@ -77,18 +79,10 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
|
|
|
77
79
|
const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
|
|
78
80
|
const panelId = `conversation-messages-${id}`;
|
|
79
81
|
const ticked = selectedConversationIds?.has(id) ?? false;
|
|
80
|
-
return (_jsxs("li", { children: [_jsxs("div", { className: [
|
|
81
|
-
"flex items-stretch border-b border-border",
|
|
82
|
-
conversation.latestMessageUid === selectedUid || ticked
|
|
83
|
-
? "bg-primary/10"
|
|
84
|
-
: "hover:bg-surface-alt",
|
|
85
|
-
].join(" "), children: [selectMode && (_jsx("span", { className: "shrink-0 flex items-center pl-3", children: _jsx("input", { type: "checkbox", checked: ticked, onChange: () => onToggleSelected?.(conversation), "aria-label": `Select conversation: ${conversation.subject || "(no subject)"}`, className: "w-4 h-4 accent-primary" }) })), _jsx("button", { type: "button", onClick: () => toggle(conversation), "aria-expanded": isExpanded, "aria-controls": panelId, "aria-label": `${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`, className: "shrink-0 px-2 text-text-muted hover:text-text", children: isExpanded ? (_jsx(HiChevronDown, { size: 16, "aria-hidden": "true" })) : (_jsx(HiChevronRight, { size: 16, "aria-hidden": "true" })) }), _jsxs("button", { type: "button", onClick: () => onOpenMessage(conversation, conversation.latestMessageUid), className: ["flex-1 min-w-0 text-left pr-4 py-3", unread ? "font-semibold" : ""].join(" "), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: conversation.participants[0] ?? conversation.latestFrom, className: "flex-1" }), conversation.participants.length > 1 && (_jsxs("span", { className: "text-xs text-text-muted shrink-0 font-normal", title: participantList(conversation), children: ["+", conversation.participants.length - 1, _jsxs("span", { className: "sr-only", children: [" more: ", participantList(conversation)] })] })), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(conversation.latestDate).toLocaleDateString() })] }), _jsx("div", { className: "text-sm truncate", children: conversation.subject || "(no subject)" }), _jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: conversation.latestPreview }), _jsxs("div", { className: "flex items-center gap-2 mt-1 text-xs text-text-muted font-normal", children: [conversation.messageCount > 1 && _jsxs("span", { children: [conversation.messageCount, " messages"] }), unread && conversation.messageCount > 1 && (_jsxs("span", { className: "py-0.5 px-2 rounded-pill bg-primary/10 text-primary-dark font-semibold", children: [conversation.unreadCount, " unread"] })), conversation.hasAttachments && (_jsx(HiOutlinePaperClip, { size: 13, "aria-label": "Has attachments" })), conversation.flagged && (_jsx(HiOutlineFlag, { size: 13, "aria-label": "Flagged", className: "text-danger" }))] })] })] }), _jsxs("ul", { id: panelId, hidden: !isExpanded, children: [errorsById[id] && (_jsx("li", { role: "alert", className: "pl-8 pr-4 py-2 text-xs text-danger border-b border-border", children: errorsById[id] })), loadingIds.has(id) && (_jsx("li", { className: "pl-8 pr-4 py-2 text-xs text-text-muted border-b border-border", children: "Loading messages\u2026" })), children?.map((fetched) => {
|
|
82
|
+
return (_jsxs("li", { children: [_jsxs("div", { "data-message-uid": conversation.latestMessageUid, "data-unread": unread ? "true" : undefined, className: rowClass({ unread, selected: conversation.latestMessageUid === selectedUid || ticked }, "flex items-stretch"), children: [_jsx(UnreadBar, { unread: unread }), selectMode && (_jsx("span", { className: "shrink-0 flex items-center pl-3", children: _jsx("input", { type: "checkbox", checked: ticked, onChange: () => onToggleSelected?.(conversation), "aria-label": `Select conversation: ${conversation.subject || "(no subject)"}`, className: "w-4 h-4 accent-primary" }) })), _jsx("button", { type: "button", onClick: () => toggle(conversation), "aria-expanded": isExpanded, "aria-controls": panelId, "aria-label": `${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`, className: ["shrink-0 px-2 text-text-muted hover:text-text", ROW_FOCUS_CLASS].join(" "), children: isExpanded ? (_jsx(HiChevronDown, { size: 16, "aria-hidden": "true" })) : (_jsx(HiChevronRight, { size: 16, "aria-hidden": "true" })) }), _jsxs("button", { type: "button", "data-row-open": true, onClick: () => onOpenMessage(conversation, conversation.latestMessageUid), className: ["flex-1 min-w-0 text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" "), children: [_jsx(UnreadLabel, { unread: unread }), _jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: conversation.participants[0] ?? conversation.latestFrom, className: ["flex-1", senderClass(unread)].join(" ") }), conversation.participants.length > 1 && (_jsxs("span", { className: "text-xs text-text-muted shrink-0 font-normal", title: participantList(conversation), children: ["+", conversation.participants.length - 1, _jsxs("span", { className: "sr-only", children: [" more: ", participantList(conversation)] })] })), _jsx("span", { className: ["text-xs shrink-0", dateClass(unread)].join(" "), children: new Date(conversation.latestDate).toLocaleDateString() })] }), _jsx("div", { className: ["text-sm truncate", subjectClass(unread)].join(" "), children: conversation.subject || "(no subject)" }), _jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: conversationLooksEncrypted(conversation) ? _jsx(EncryptedPreview, {}) : conversation.latestPreview }), _jsxs("div", { className: "flex items-center gap-2 mt-1 text-xs text-text-muted font-normal", children: [conversation.messageCount > 1 && _jsxs("span", { children: [conversation.messageCount, " messages"] }), unread && conversation.messageCount > 1 && (_jsxs("span", { className: "py-0.5 px-2 rounded-pill bg-primary/15 text-primary-dark font-bold", children: [conversation.unreadCount, " unread"] })), conversation.hasAttachments && (_jsx(HiOutlinePaperClip, { size: 13, "aria-label": "Has attachments" })), conversation.flagged && (_jsx(HiOutlineFlag, { size: 13, "aria-label": "Flagged", className: "text-danger" }))] })] })] }), _jsxs("ul", { id: panelId, hidden: !isExpanded, children: [errorsById[id] && (_jsx("li", { role: "alert", className: "pl-8 pr-4 py-2 text-xs text-danger border-b border-border", children: errorsById[id] })), loadingIds.has(id) && (_jsx("li", { className: "pl-8 pr-4 py-2 text-xs text-text-muted border-b border-border", children: "Loading messages\u2026" })), children?.map((fetched) => {
|
|
86
83
|
const message = messageOverrides?.[fetched.uid] ?? fetched;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
message.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt",
|
|
90
|
-
message.flags.read ? "" : "font-semibold",
|
|
91
|
-
].join(" "), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: message.from }), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(message.receivedDate).toLocaleDateString() })] }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-muted font-normal", children: [_jsx("span", { className: "truncate", children: message.bodyPreview }), message.hasAttachments && (_jsx(HiOutlinePaperClip, { size: 12, "aria-label": "Has attachments" })), message.flags.flagged && (_jsx(HiOutlineFlag, { size: 12, "aria-label": "Flagged", className: "text-danger" }))] })] }) }, message.uid));
|
|
84
|
+
const messageUnread = isUnread(message);
|
|
85
|
+
return (_jsxs("li", { "data-message-uid": message.uid, "data-unread": messageUnread ? "true" : undefined, className: rowClass({ unread: messageUnread, selected: message.uid === selectedUid }), children: [_jsx(UnreadBar, { unread: messageUnread }), _jsxs("button", { type: "button", "data-row-open": true, onClick: () => onOpenMessage(conversation, message.uid), className: ["w-full text-left pl-8 pr-4 py-2", ROW_FOCUS_CLASS].join(" "), children: [_jsx(UnreadLabel, { unread: messageUnread }), _jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: message.from, className: senderClass(messageUnread) }), _jsx("span", { className: ["text-xs shrink-0", dateClass(messageUnread)].join(" "), children: new Date(message.receivedDate).toLocaleDateString() })] }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-muted font-normal", children: [_jsx("span", { className: "truncate", children: message.bodyPreview || (message.encrypted ? _jsx(EncryptedPreview, {}) : null) }), message.hasAttachments && (_jsx(HiOutlinePaperClip, { size: 12, "aria-label": "Has attachments" })), message.flags.flagged && (_jsx(HiOutlineFlag, { size: 12, "aria-label": "Flagged", className: "text-danger" }))] })] })] }, message.uid));
|
|
92
86
|
})] })] }, id));
|
|
93
87
|
}) }));
|
|
94
88
|
}
|
|
@@ -41,9 +41,13 @@ export interface ConversationThreadPaneProps {
|
|
|
41
41
|
onLabelCreated?: (label: Label) => void;
|
|
42
42
|
/** A folder created from a message's own Move to prompt, for the folder sidebar to pick up. */
|
|
43
43
|
onFolderCreated?: (folder: Folder) => void;
|
|
44
|
+
/** Registers the keyboard shortcuts (Reply, Reply all, Forward, Archive, Move to) of the message that was opened - see `MessageDetailPane`'s
|
|
45
|
+
* `shortcuts`. The caller turns it off while it is doing something else with the keyboard (select mode). */
|
|
46
|
+
shortcuts?: boolean;
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
|
-
* The reading pane for the conversation list: the
|
|
49
|
+
* The reading pane for the conversation list: the conversation's subject in a card of its own, pinned at the top, and under it the whole thread as
|
|
50
|
+
* a stack of message cards (each exactly as tall as its message; the list scrolls as a whole), **newest at the top**, opened at the message
|
|
47
51
|
* the reader picked. Every message from that one through to the newest is expanded - which in this order is
|
|
48
52
|
* the opened message and the entries above it - and the older ones, below it, are collapsed to a one-line
|
|
49
53
|
* summary (sender, date, preview) that expands on click or Enter. So opening the newest message shows just
|
|
@@ -59,4 +63,4 @@ export interface ConversationThreadPaneProps {
|
|
|
59
63
|
* single-message pane, and each acts on the message it belongs to. A collapsed message mounts none of
|
|
60
64
|
* that - mounting a body iframe per message up front would be wasteful in a long thread.
|
|
61
65
|
*/
|
|
62
|
-
export default function ConversationThreadPane({ conversation, mailboxUid, selectedUid, folders, labels, onMessagePatched, onMessageRemoved, onLabelCreated, onFolderCreated, }: ConversationThreadPaneProps): React.JSX.Element;
|
|
66
|
+
export default function ConversationThreadPane({ conversation, mailboxUid, selectedUid, folders, labels, onMessagePatched, onMessageRemoved, onLabelCreated, onFolderCreated, shortcuts, }: ConversationThreadPaneProps): React.JSX.Element;
|