@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
|
@@ -114,5 +114,5 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
114
114
|
setSaving(false);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "max-w-xl flex
|
|
117
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "@container flex min-h-0 flex-1 flex-col", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsxs("div", { className: "flex max-w-xl flex-col gap-1", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-3", children: contact ? "Edit contact" : "New contact" }), error && _jsx(Alert, { children: error }), !contact && mailboxes && mailboxes.length > 1 && (_jsx(FormField, { label: "Mailbox", htmlFor: "contact-mailbox", children: _jsx("select", { id: "contact-mailbox", className: `${SELECT_CLASS} w-full`, value: targetMailboxUid, onChange: (e) => setTargetMailboxUid(e.target.value), children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) }) })), _jsx(FormField, { label: "Display name", htmlFor: "contact-displayName", children: _jsx("input", { id: "contact-displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-1 @md:grid-cols-2 gap-3", children: [_jsx(FormField, { label: "First name", htmlFor: "contact-givenName", children: _jsx("input", { id: "contact-givenName", type: "text", className: INPUT_CLASS, value: givenName, onChange: (e) => setGivenName(e.target.value) }) }), _jsx(FormField, { label: "Last name", htmlFor: "contact-surname", children: _jsx("input", { id: "contact-surname", type: "text", className: INPUT_CLASS, value: surname, onChange: (e) => setSurname(e.target.value) }) })] }), _jsxs("div", { className: "grid grid-cols-1 @md:grid-cols-2 gap-3", children: [_jsx(FormField, { label: "Company", htmlFor: "contact-company", children: _jsx("input", { id: "contact-company", type: "text", className: INPUT_CLASS, value: company, onChange: (e) => setCompany(e.target.value) }) }), _jsx(FormField, { label: "Job title", htmlFor: "contact-jobTitle", children: _jsx("input", { id: "contact-jobTitle", type: "text", className: INPUT_CLASS, value: jobTitle, onChange: (e) => setJobTitle(e.target.value) }) })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: favorite, onChange: (e) => setFavorite(e.target.checked) }), "Favorite"] }), _jsx(FormField, { label: "Categories (comma-separated)", htmlFor: "contact-categories", children: _jsx("input", { id: "contact-categories", type: "text", className: INPUT_CLASS, value: categoriesText, onChange: (e) => setCategoriesText(e.target.value), placeholder: "VIP, Work" }) }), _jsx(FormField, { label: "Email", htmlFor: "contact-emails", children: _jsxs("div", { id: "contact-emails", className: "flex flex-col gap-2", children: [emails.map((email, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "email", className: `${INPUT_CLASS} flex-1`, value: email.address, onChange: (e) => updateEmail(i, { address: e.target.value }), "aria-label": `Email address ${i + 1}` }), _jsx("select", { className: SELECT_CLASS, value: email.type, onChange: (e) => updateEmail(i, { type: e.target.value }), "aria-label": `Email type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) }), _jsx("button", { type: "button", onClick: () => removeEmail(i), className: "text-sm text-danger px-2", "aria-label": `Remove email ${i + 1}`, children: "\u00D7" })] }, i))), _jsx("button", { type: "button", onClick: () => setEmails((prev) => [...prev, { address: "", type: "work" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add email" })] }) }), _jsx(FormField, { label: "Phone", htmlFor: "contact-phones", children: _jsxs("div", { id: "contact-phones", className: "flex flex-col gap-2", children: [phones.map((phone, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "tel", className: `${INPUT_CLASS} flex-1`, value: phone.phoneNumber, onChange: (e) => updatePhone(i, { phoneNumber: e.target.value }), "aria-label": `Phone number ${i + 1}` }), _jsx("select", { className: SELECT_CLASS, value: phone.type, onChange: (e) => updatePhone(i, { type: e.target.value }), "aria-label": `Phone type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) }), _jsx("button", { type: "button", onClick: () => removePhone(i), className: "text-sm text-danger px-2", "aria-label": `Remove phone ${i + 1}`, children: "\u00D7" })] }, i))), _jsx("button", { type: "button", onClick: () => setPhones((prev) => [...prev, { phoneNumber: "", type: "work" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add phone" })] }) }), _jsx(FormField, { label: "Address", htmlFor: "contact-address", children: _jsxs("div", { className: "flex flex-col gap-3", children: [addresses.map((address, i) => (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-1 @md:grid-cols-2 gap-2", children: [_jsx("input", { type: "text", placeholder: "Street", className: INPUT_CLASS, value: address.street ?? "", onChange: (e) => updateAddress(i, { street: e.target.value }) }), _jsx("input", { type: "text", placeholder: "City", className: INPUT_CLASS, value: address.city ?? "", onChange: (e) => updateAddress(i, { city: e.target.value }) }), _jsx("input", { type: "text", placeholder: "State/Province", className: INPUT_CLASS, value: address.state ?? "", onChange: (e) => updateAddress(i, { state: e.target.value }) }), _jsx("input", { type: "text", placeholder: "Postal code", className: INPUT_CLASS, value: address.postalCode ?? "", onChange: (e) => updateAddress(i, { postalCode: e.target.value }) }), _jsx("input", { type: "text", placeholder: "Country", className: INPUT_CLASS, value: address.country ?? "", onChange: (e) => updateAddress(i, { country: e.target.value }) }), _jsx("select", { className: SELECT_CLASS, value: address.type, onChange: (e) => updateAddress(i, { type: e.target.value }), "aria-label": i === 0 ? "Address type" : `Address type ${i + 1}`, children: ADDRESS_KINDS.map((kind) => (_jsx("option", { value: kind, children: kind }, kind))) })] }), _jsx("button", { type: "button", onClick: () => removeAddress(i), className: "self-start text-xs font-medium text-danger hover:underline", "aria-label": i === 0 ? "Remove address" : `Remove address ${i + 1}`, children: "Remove address" })] }, i))), _jsx("button", { type: "button", onClick: () => setAddresses((prev) => [...prev, { type: "home" }]), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add address" })] }) }), _jsx(FormField, { label: "Notes", htmlFor: "contact-notes", children: _jsx("textarea", { id: "contact-notes", rows: 3, className: INPUT_CLASS, value: notes, onChange: (e) => setNotes(e.target.value) }) })] }) }), _jsxs("div", { className: "rr-solid sticky bottom-14 z-10 flex shrink-0 gap-3 border-t border-border bg-surface px-6 py-3 md:bottom-0", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onCancel, children: "Cancel" })] })] }));
|
|
118
118
|
}
|
|
@@ -8,6 +8,7 @@ import { HiOutlineBars3 } from "react-icons/hi2";
|
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { createContactList, listContactLists } from "@rapidmx/react-shared/contacts/contactsApi.js";
|
|
10
10
|
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
11
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
11
12
|
export function contactsViewKey(view) {
|
|
12
13
|
switch (view.type) {
|
|
13
14
|
case "list":
|
|
@@ -90,7 +91,8 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
|
|
|
90
91
|
setAddingList(false);
|
|
91
92
|
}
|
|
92
93
|
catch (err) {
|
|
93
|
-
|
|
94
|
+
// A pop-up: `listsError` is only the lists' own load failure.
|
|
95
|
+
notifyApiError(err, "Couldn't create the list");
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
function handleSelect(view) {
|
|
@@ -101,5 +103,5 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
|
|
|
101
103
|
// `Drawer` (see MailShell/ContactsShell for the identical pattern and the duplicate-id reasoning
|
|
102
104
|
// behind the `idPrefix`).
|
|
103
105
|
const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "Your contacts", count: contacts.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsx(NavItem, { label: "Favorites", count: favoriteCount, active: active.type === "favorites", onClick: () => handleSelect({ type: "favorites" }) }), showDeleted && _jsx(NavItem, { label: "Deleted", active: active.type === "deleted", onClick: () => handleSelect({ type: "deleted" }) })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Your contact lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New contact list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] }), categories.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted px-2.5 mb-1", children: "Categories" }), _jsx("div", { className: "flex flex-col gap-0.5", children: categories.map((name) => (_jsx(NavItem, { label: name, swatch: categoryColor(name), active: active.type === "category" && active.name === name, onClick: () => handleSelect({ type: "category", name }) }, name))) })] }))] }));
|
|
104
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "
|
|
106
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open contacts menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Contacts", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Contacts", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
|
|
105
107
|
}
|
|
@@ -14,12 +14,16 @@ export interface ContactsToolbarProps {
|
|
|
14
14
|
/** A `.vcf` file the user picked via the toolbar's own hidden file input — parsing/import is the
|
|
15
15
|
* page's responsibility (it has the mailbox/folder context and the actual `createContact` calls). */
|
|
16
16
|
onImportFile: (file: File) => void;
|
|
17
|
+
/** The keyboard's "New contact" is registered by the page: the button names its shortcut in its tooltip and `aria-keyshortcuts`. */
|
|
18
|
+
shortcuts?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
|
-
* Contacts' Outlook-style ribbon toolbar
|
|
21
|
+
* Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
|
|
22
|
+
* alone, then the rarely used actions - Import, Export, Add category, and so on up - in a "More" menu; the bar can never spill into the pane
|
|
23
|
+
* beside it. Every button keeps its tooltip (New contact its shortcut's) and accessible name in every layout. Deliberately presentational — every action is a callback the
|
|
20
24
|
* page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
|
|
21
25
|
* selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
|
|
22
26
|
* needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
|
|
23
27
|
* "+ New list" affordance next to the "Your contact lists" heading.
|
|
24
28
|
*/
|
|
25
|
-
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, }: ContactsToolbarProps): React.JSX.Element;
|
|
29
|
+
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, }: ContactsToolbarProps): React.JSX.Element;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useRef } from "react";
|
|
7
7
|
import { HiOutlineArrowDownTray, HiOutlineArrowUpTray, HiOutlineEnvelope, HiOutlinePencil, HiOutlineStar, HiOutlineTag, HiOutlineTrash, HiOutlineUserPlus, HiStar, } from "react-icons/hi2";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function Divider() {
|
|
12
|
-
return _jsx("div", { className: "w-px self-stretch my-1 bg-border", "aria-hidden": "true" });
|
|
13
|
-
}
|
|
8
|
+
import { SHORTCUTS } from "../../keyboard/keymap.js";
|
|
9
|
+
import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
|
|
10
|
+
import ResponsiveToolbar from "../layout/ResponsiveToolbar.js";
|
|
14
11
|
/**
|
|
15
|
-
* Contacts' Outlook-style ribbon toolbar
|
|
12
|
+
* Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
|
|
13
|
+
* alone, then the rarely used actions - Import, Export, Add category, and so on up - in a "More" menu; the bar can never spill into the pane
|
|
14
|
+
* beside it. Every button keeps its tooltip (New contact its shortcut's) and accessible name in every layout. Deliberately presentational — every action is a callback the
|
|
16
15
|
* page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
|
|
17
16
|
* selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
|
|
18
17
|
* needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
|
|
19
18
|
* "+ New list" affordance next to the "Your contact lists" heading.
|
|
20
19
|
*/
|
|
21
|
-
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, }) {
|
|
20
|
+
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, }) {
|
|
21
|
+
const newContactHint = useShortcutProps("New contact", SHORTCUTS.contacts.create, !!shortcuts);
|
|
22
22
|
const importInputRef = useRef(null);
|
|
23
23
|
const hasSelection = selectedCount > 0;
|
|
24
24
|
function handleImportFileChosen(e) {
|
|
@@ -28,5 +28,23 @@ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, o
|
|
|
28
28
|
onImportFile(file);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const actions = [
|
|
32
|
+
{ id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true },
|
|
33
|
+
{ id: "edit", label: "Edit", icon: HiOutlinePencil, onClick: onEdit, disabled: selectedCount !== 1, group: 1, rank: 6 },
|
|
34
|
+
{ id: "delete", label: "Delete", icon: HiOutlineTrash, onClick: onDelete, disabled: !hasSelection, group: 1, rank: 5 },
|
|
35
|
+
{ id: "email", label: "Email", icon: HiOutlineEnvelope, onClick: onEmail, disabled: !hasSelection, group: 2, rank: 4 },
|
|
36
|
+
{
|
|
37
|
+
id: "favorite",
|
|
38
|
+
label: allSelectedFavorited ? "Unfavorite" : "Favorite",
|
|
39
|
+
icon: allSelectedFavorited ? HiStar : HiOutlineStar,
|
|
40
|
+
onClick: onToggleFavorite,
|
|
41
|
+
disabled: !hasSelection,
|
|
42
|
+
group: 2,
|
|
43
|
+
rank: 3,
|
|
44
|
+
},
|
|
45
|
+
{ id: "category", label: "Add category", icon: HiOutlineTag, onClick: onAddCategory, disabled: !hasSelection, group: 2, rank: 2 },
|
|
46
|
+
{ id: "export", label: "Export", icon: HiOutlineArrowUpTray, onClick: onExportVCard, disabled: !hasSelection, group: 3, rank: 1 },
|
|
47
|
+
{ id: "import", label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click(), group: 3, rank: 0 },
|
|
48
|
+
];
|
|
49
|
+
return (_jsx(ResponsiveToolbar, { label: "Contacts actions", actions: actions, children: _jsx("input", { ref: importInputRef, type: "file", accept: ".vcf", className: "sr-only", "aria-label": "Import contacts file", onChange: handleImportFileChosen }) }));
|
|
32
50
|
}
|
|
@@ -18,4 +18,4 @@ export declare function useContactsShell(): ContactsShellContextValue;
|
|
|
18
18
|
* sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
|
|
19
19
|
* list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
|
|
20
20
|
*/
|
|
21
|
-
export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<ContactsShellProps>): React.JSX.Element;
|
|
21
|
+
export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<ContactsShellProps>): React.JSX.Element;
|
|
@@ -11,6 +11,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
|
|
|
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
|
const ContactsShellContext = createContext({ mailboxes: [] });
|
|
16
17
|
/** Reads the mailbox/folder Contacts is currently showing, as resolved by the enclosing `ContactsShell`. */
|
|
@@ -24,7 +25,7 @@ export function useContactsShell() {
|
|
|
24
25
|
* sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
|
|
25
26
|
* list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
|
|
26
27
|
*/
|
|
27
|
-
export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
28
|
+
export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
28
29
|
const [status, setStatus] = useState("checking");
|
|
29
30
|
const [error, setError] = useState(null);
|
|
30
31
|
const [mailboxes, setMailboxes] = useState([]);
|
|
@@ -32,9 +33,18 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
|
|
|
32
33
|
const [folderError, setFolderError] = useState(null);
|
|
33
34
|
const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
|
|
34
35
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
36
|
+
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
37
|
+
// effect, not during render, so the server render and the hydrating render agree.
|
|
38
|
+
const search = useLocationSearch();
|
|
39
|
+
const navigate = useNavigate();
|
|
35
40
|
useEffect(() => {
|
|
36
|
-
setRequestedMailboxUid(new URLSearchParams(
|
|
37
|
-
}, []);
|
|
41
|
+
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
42
|
+
}, [search]);
|
|
43
|
+
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
44
|
+
// with the page - is closed here.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setDrawerOpen(false);
|
|
47
|
+
}, [search]);
|
|
38
48
|
useEffect(() => {
|
|
39
49
|
if (!userUid) {
|
|
40
50
|
return;
|
|
@@ -85,9 +95,12 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
|
|
|
85
95
|
// `id`/its `<label>`'s `htmlFor` need a distinct value per instance to avoid a duplicate-id
|
|
86
96
|
// label-association break.
|
|
87
97
|
const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold uppercase tracking-wide text-text-muted mb-1", htmlFor: `${idPrefix}-contacts-mailbox-switcher`, children: "Mailbox" }), _jsx("select", { id: `${idPrefix}-contacts-mailbox-switcher`, className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: mailboxUid, onChange: (e) => {
|
|
88
|
-
|
|
98
|
+
navigate(`/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`);
|
|
89
99
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
|
|
90
|
-
|
|
100
|
+
// The mailbox switcher (or an error) is all this column holds: with a single mailbox and no error there is nothing to put in it, and an empty
|
|
101
|
+
// 224 px column beside the contacts' own menu is wasted room.
|
|
102
|
+
const hasSidebar = mailboxes.length > 1 || !!folderError;
|
|
103
|
+
inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg: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: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(ContactsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
91
104
|
}
|
|
92
|
-
return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
|
|
105
|
+
return (_jsx(AppShell, { active: "contacts", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
93
106
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -13,6 +13,8 @@ import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
|
13
13
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
14
|
import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
15
15
|
import UserMenu from "../../layout/UserMenu.js";
|
|
16
|
+
import RailIcon from "../../layout/RailIcon.js";
|
|
17
|
+
import AppearanceProvider from "../../../appearance/AppearanceProvider.js";
|
|
16
18
|
import { signOutOfConsole } from "../../admin/signOut.js";
|
|
17
19
|
const DEFAULT_ICON_SRC = "/images/logo.svg";
|
|
18
20
|
const NAV_ITEMS = [
|
|
@@ -83,12 +85,13 @@ export default function EscrowShell({ active, userUid, authServerUrl, children }
|
|
|
83
85
|
}
|
|
84
86
|
else {
|
|
85
87
|
const activeItem = NAV_ITEMS.find((item) => item.id === active);
|
|
86
|
-
content = (_jsx("div", { className: "min-h-screen flex flex-col
|
|
88
|
+
content = (_jsx("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Escrow sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center pb-3 gap-1", children: [_jsx(RailIcon, { src: iconSrc }), NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
|
|
87
89
|
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
88
90
|
id === active
|
|
89
91
|
? "bg-primary/10 text-primary-dark"
|
|
90
92
|
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
91
|
-
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
|
|
93
|
+
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { id: "app-content", className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
// The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
|
|
96
|
+
return (_jsx(AppearanceProvider, { userUid: userUid, lookUp: false, children: content }));
|
|
94
97
|
}
|
|
@@ -2,6 +2,7 @@ import "../../styles/app.css";
|
|
|
2
2
|
import React, { PropsWithChildren } from "react";
|
|
3
3
|
import type { IconType } from "react-icons";
|
|
4
4
|
import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
5
|
+
import type { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
5
6
|
import { PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
|
|
6
7
|
/** How long sign-out waits for auth-server's logout before navigating anyway. */
|
|
7
8
|
export declare const LOGOUT_TIMEOUT_MS = 3000;
|
|
@@ -27,8 +28,17 @@ export interface AppShellProps extends PluginNavProps {
|
|
|
27
28
|
impersonating?: boolean;
|
|
28
29
|
/** Where `mailApi.ts`'s `stopImpersonating()` should call — see `MailShell`'s original doc comment. */
|
|
29
30
|
impersonationBaseUrl?: string;
|
|
30
|
-
/** `true` when the caller's JWT carries a trusted role — shows an "Admin" item in the user menu below.
|
|
31
|
+
/** `true` when the caller's JWT carries a trusted role — shows an "Admin Console" item in the user menu below. A token
|
|
32
|
+
* only carries one once elevated, so a real administrator with an ordinary session is `false` here; the user menu
|
|
33
|
+
* asks auth-server about them separately (see `UserMenu`'s `detectAdmin`). */
|
|
31
34
|
trusted?: boolean;
|
|
35
|
+
/** The role names the server treats as trusted (its `trusted_roles` config, injected via the route's `fetchProps`) -
|
|
36
|
+
* what that lookup looks for. Absent means the server's own default, `["admin"]`. */
|
|
37
|
+
trustedRoles?: string[];
|
|
38
|
+
/** The deployment's branding as the server rendered the page (its `branding` prop). Used until the frame's own fetch answers, so a custom header is there from the first paint - and the frame knows to drop its own title bar - instead of appearing (and moving everything) once the request returns. */
|
|
39
|
+
branding?: Branding;
|
|
40
|
+
/** The signed-in user's stored appearance preferences as the server rendered the page (its `appearance` prop), so the first paint already wears the theme and background - see `AppearanceProvider`. */
|
|
41
|
+
appearance?: unknown;
|
|
32
42
|
}
|
|
33
43
|
export interface AppDef {
|
|
34
44
|
id: AppShellApp;
|
|
@@ -39,11 +49,31 @@ export interface AppDef {
|
|
|
39
49
|
export declare const APPS: AppDef[];
|
|
40
50
|
/** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
|
|
41
51
|
export declare function appRailItems(pluginNav?: PluginNav): NavItem[];
|
|
52
|
+
/** What only the persistent frame (`AppRouter`) passes to the chrome it keeps mounted. */
|
|
53
|
+
export interface AppChromeProps extends AppShellProps {
|
|
54
|
+
/** Changes whenever the router shows a different page: the chrome then moves focus to the content, scrolls to its top,
|
|
55
|
+
* sets the document title and announces the page. Absent outside the router, when nothing does. */
|
|
56
|
+
routeKey?: string;
|
|
57
|
+
/** The router is loading the next page's code - the content is `aria-busy`. */
|
|
58
|
+
busy?: boolean;
|
|
59
|
+
/** A screen that takes over the window (`FrameTakeover`) is showing: the rail, header, banner and footer are hidden - not
|
|
60
|
+
* unmounted, so what is inside them (and the page in the content region) keeps its state. */
|
|
61
|
+
hideChrome?: boolean;
|
|
62
|
+
}
|
|
42
63
|
/**
|
|
43
64
|
* The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
|
|
44
|
-
* switching apps
|
|
45
|
-
* client-side router), a header with the current app's name and `UserMenu`, and the impersonation banner.
|
|
65
|
+
* switching apps, a header with the current app's name and `UserMenu`, and the impersonation banner.
|
|
46
66
|
* Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
|
|
47
67
|
* the area to the right of the icon rail and below the header.
|
|
68
|
+
*
|
|
69
|
+
* Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
|
|
70
|
+
* `AppShell` below for what a page's own shell renders instead.
|
|
71
|
+
*/
|
|
72
|
+
export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
|
|
73
|
+
/**
|
|
74
|
+
* What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
|
|
75
|
+
* is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
|
|
76
|
+
* mounted as the page is replaced - so this is only its children; everything it would have been given comes from the router
|
|
77
|
+
* frame instead (the props are the same for every page, and `active` is the route's).
|
|
48
78
|
*/
|
|
49
|
-
export default function AppShell(
|
|
79
|
+
export default function AppShell(props: PropsWithChildren<AppShellProps>): React.JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -14,14 +14,33 @@ import { useIdleKeyTimeout } from "@rapidmx/react-shared/crypto/useIdleKeyTimeou
|
|
|
14
14
|
import ComposeProvider from "../mail/compose/ComposeContext.js";
|
|
15
15
|
import { flushComposeDrafts, markSigningOut } from "../mail/compose/composeFlushRegistry.js";
|
|
16
16
|
import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
17
|
-
import {
|
|
17
|
+
import { FrameBrandingFooter, FrameBrandingHeader, useBrandingHtml } from "./BrandingChrome.js";
|
|
18
|
+
import RailIcon from "./RailIcon.js";
|
|
19
|
+
import AppearanceProvider from "../../appearance/AppearanceProvider.js";
|
|
20
|
+
import { clearAppearanceCache } from "../../appearance/appearanceCache.js";
|
|
18
21
|
import UserMenu from "./UserMenu.js";
|
|
19
22
|
import { UnlockPromptProvider } from "./UnlockPromptProvider.js";
|
|
20
23
|
import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
|
|
21
|
-
import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
|
|
24
|
+
import { authApiFetch, setApiUnauthorizedObserver } from "@rapidmx/react-shared/util/api.js";
|
|
22
25
|
import { destroyUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
23
26
|
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
24
27
|
import { mergePluginNavItems } from "../../plugins/pluginNav.js";
|
|
28
|
+
import { useInAppFrame } from "../../navigation/frameContext.js";
|
|
29
|
+
import { APP_HREFS } from "../../navigation/appHrefs.js";
|
|
30
|
+
import { useNavigate } from "../../navigation/routerContext.js";
|
|
31
|
+
import { GlobalShortcuts } from "../../keyboard/GlobalShortcuts.js";
|
|
32
|
+
import { ShortcutProvider } from "../../keyboard/ShortcutProvider.js";
|
|
33
|
+
import ShortcutsDialog from "../../keyboard/ShortcutsDialog.js";
|
|
34
|
+
import { inboxUnreadTotal } from "../../mail/folderCounts.js";
|
|
35
|
+
import { MailConnectionContext, useMailConnection } from "../../mail/useMailConnection.js";
|
|
36
|
+
import { useUnreadTitle } from "../../mail/useUnreadTitle.js";
|
|
37
|
+
import UnlockBridge from "../../mail/outbox/UnlockBridge.js";
|
|
38
|
+
import NotificationCenter from "../../notifications/NotificationCenter.js";
|
|
39
|
+
import { useHeaderHeightRef } from "../../notifications/headerOffset.js";
|
|
40
|
+
import NotificationHistoryDialog from "../../notifications/NotificationHistoryDialog.js";
|
|
41
|
+
import { notifySessionExpired, setSignInUrl } from "../../notifications/apiErrors.js";
|
|
42
|
+
import { useUnseenErrorCount } from "../../notifications/useNotifications.js";
|
|
43
|
+
import { useSigningEnrollmentWatcher } from "../../signing/useSigningEnrollmentWatcher.js";
|
|
25
44
|
/** How long sign-out waits for auth-server's logout before navigating anyway. */
|
|
26
45
|
export const LOGOUT_TIMEOUT_MS = 3000;
|
|
27
46
|
/**
|
|
@@ -46,10 +65,10 @@ async function logOutOfAuthServer(authServerUrl) {
|
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
export const APPS = [
|
|
49
|
-
{ id: "mail", href:
|
|
50
|
-
{ id: "calendar", href:
|
|
51
|
-
{ id: "contacts", href:
|
|
52
|
-
{ id: "tasks", href:
|
|
68
|
+
{ id: "mail", href: APP_HREFS.mail, label: "Mail", icon: HiOutlineEnvelope },
|
|
69
|
+
{ id: "calendar", href: APP_HREFS.calendar, label: "Calendar", icon: HiOutlineCalendarDays },
|
|
70
|
+
{ id: "contacts", href: APP_HREFS.contacts, label: "Contacts", icon: HiOutlineUsers },
|
|
71
|
+
{ id: "tasks", href: APP_HREFS.tasks, label: "Tasks", icon: HiOutlineClipboardDocumentList },
|
|
53
72
|
];
|
|
54
73
|
/** Ids plugin `appRail` items can't take besides `APPS`' own - `"settings"` has no rail icon but is still a
|
|
55
74
|
* core `active` value. */
|
|
@@ -60,16 +79,51 @@ export function appRailItems(pluginNav) {
|
|
|
60
79
|
}
|
|
61
80
|
/**
|
|
62
81
|
* The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
|
|
63
|
-
* switching apps
|
|
64
|
-
* client-side router), a header with the current app's name and `UserMenu`, and the impersonation banner.
|
|
82
|
+
* switching apps, a header with the current app's name and `UserMenu`, and the impersonation banner.
|
|
65
83
|
* Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
|
|
66
84
|
* the area to the right of the icon rail and below the header.
|
|
85
|
+
*
|
|
86
|
+
* Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
|
|
87
|
+
* `AppShell` below for what a page's own shell renders instead.
|
|
67
88
|
*/
|
|
68
|
-
export
|
|
89
|
+
export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }) {
|
|
69
90
|
const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
|
|
91
|
+
// The keyboard shortcuts dialog: opened by `?`/Ctrl+/ (`GlobalShortcuts`) and by the account menu's item.
|
|
92
|
+
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
93
|
+
// The pop-up history ("Recent notifications" in the account menu). The pop-ups themselves are drawn by `NotificationCenter`, below.
|
|
94
|
+
const [historyOpen, setHistoryOpen] = useState(false);
|
|
95
|
+
// Only the count is read, so the whole frame does not render again for every pop-up that comes and goes.
|
|
96
|
+
const unseenErrors = useUnseenErrorCount();
|
|
97
|
+
// The title bar's height for the pop-up stack, which sticks just below the header (a branding header publishes its own - `FrameBrandingHeader`).
|
|
98
|
+
const headerRef = useHeaderHeightRef();
|
|
70
99
|
const signingOutRef = useRef(false);
|
|
71
|
-
const { branding, iconSrc } = useBranding();
|
|
100
|
+
const { branding: fetchedBranding, iconSrc: fetchedIconSrc } = useBranding();
|
|
101
|
+
// The server's copy until the fetch answers, so the frame's shape (a custom header replaces the title bar) never changes after the first paint.
|
|
102
|
+
const branding = fetchedBranding ?? initialBranding ?? null;
|
|
103
|
+
const iconSrc = fetchedBranding ? fetchedIconSrc : initialBranding?.iconUrl || initialBranding?.logoUrl || fetchedIconSrc;
|
|
104
|
+
// The custom header and footer, sanitized and with their `{USER_MENU}` / `{APP_TITLE}` placeholders (`undefined` until parsed, `null` when there is none).
|
|
105
|
+
const header = useBrandingHtml(branding?.headerHtml);
|
|
106
|
+
const footer = useBrandingHtml(branding?.footerHtml);
|
|
107
|
+
// The mailboxes, their folders and the one push connection live here, in the frame that stays mounted as the router swaps pages - so
|
|
108
|
+
// new-mail pop-ups, the folder counters and the tab title's unread count work in Calendar, Contacts, Tasks and Settings too, and moving
|
|
109
|
+
// between apps never opens a second socket (the server allows ten per user). `MailShell` reads them from `MailConnectionContext`. Outside
|
|
110
|
+
// the router (`AppShell` rendered as a page's own chrome: tests, plugin pages) it stays off, and a Mail shell owns its connection itself.
|
|
111
|
+
const inFrame = useInAppFrame();
|
|
112
|
+
const navigate = useNavigate();
|
|
113
|
+
const mail = useMailConnection({ userUid, enabled: inFrame, open: navigate });
|
|
114
|
+
// A signing certificate the user asked for is announced when it is issued (or fails), on whichever page they are - see the hook.
|
|
115
|
+
useSigningEnrollmentWatcher({ userUid, mailboxes: mail.mailboxes, enabled: inFrame });
|
|
72
116
|
useRedirectIfUnauthenticated(userUid, authServerUrl);
|
|
117
|
+
// Any request this app makes that the server answers with a 401 - the session ended - raises one "Your session expired" pop-up with a
|
|
118
|
+
// Sign in action (see `notifySessionExpired()`), whichever request noticed first, a background refresh included.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setSignInUrl(authServerUrl);
|
|
121
|
+
if (!userUid) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setApiUnauthorizedObserver(() => void notifySessionExpired());
|
|
125
|
+
return () => setApiUnauthorizedObserver(undefined);
|
|
126
|
+
}, [userUid, authServerUrl]);
|
|
73
127
|
// Mounted here, not scoped to Mail/Settings (the only shells that actually read unlocked keys),
|
|
74
128
|
// specifically so activity in *any* app resets the idle clock - see that hook's own doc comment.
|
|
75
129
|
useIdleKeyTimeout();
|
|
@@ -109,6 +163,7 @@ export default function AppShell({ active, userUid, authServerUrl, impersonating
|
|
|
109
163
|
markSigningOut();
|
|
110
164
|
destroyUnlockedKeys();
|
|
111
165
|
clearPinnedSignerCache();
|
|
166
|
+
clearAppearanceCache();
|
|
112
167
|
// Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
|
|
113
168
|
void destroyAllLocalIndexes().then(() => {
|
|
114
169
|
window.location.href = authServerUrl ?? "/";
|
|
@@ -125,6 +180,7 @@ export default function AppShell({ active, userUid, authServerUrl, impersonating
|
|
|
125
180
|
destroyUnlockedKeys();
|
|
126
181
|
// Trusted signer pins read from contacts don't outlive the session either.
|
|
127
182
|
clearPinnedSignerCache();
|
|
183
|
+
clearAppearanceCache();
|
|
128
184
|
// Open compose windows save edits still waiting on their autosave debounce while the session is still
|
|
129
185
|
// valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
|
|
130
186
|
await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
|
|
@@ -151,16 +207,65 @@ export default function AppShell({ active, userUid, authServerUrl, impersonating
|
|
|
151
207
|
window.location.href = "/admin";
|
|
152
208
|
}
|
|
153
209
|
}
|
|
210
|
+
// What the persistent frame (`AppRouter`) needs on top of a page that never changes: the document title follows the page, and
|
|
211
|
+
// after the router replaces the page the keyboard focus, the scroll position and a screen reader are told about it - a
|
|
212
|
+
// page load did all of that by itself. Nothing here runs for a page shown outside the router (`routeKey` is undefined).
|
|
213
|
+
const contentRef = useRef(null);
|
|
214
|
+
const [announcement, setAnnouncement] = useState("");
|
|
215
|
+
const firstRouteKeyRef = useRef(routeKey);
|
|
216
|
+
const pageLabel = active === "settings" ? "Settings" : appRailItems(pluginNav).find((app) => app.id === active)?.label;
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (routeKey !== undefined) {
|
|
219
|
+
const brand = branding?.title || branding?.companyName || "RapidMX";
|
|
220
|
+
document.title = pageLabel ? `${brand}: ${pageLabel}` : brand;
|
|
221
|
+
}
|
|
222
|
+
}, [routeKey, pageLabel, branding]);
|
|
223
|
+
// After the title effect above, so a page change (which rewrites the title) is followed by the unread count going back in front of it.
|
|
224
|
+
useUnreadTitle(inboxUnreadTotal(mail.mailboxFolders, mail.folderCounts.counts), {
|
|
225
|
+
enabled: inFrame,
|
|
226
|
+
resetKey: `${routeKey}|${pageLabel}|${branding?.title}|${branding?.companyName}`,
|
|
227
|
+
});
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (routeKey === undefined || routeKey === firstRouteKeyRef.current) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
setAnnouncement(pageLabel ?? "");
|
|
233
|
+
contentRef.current?.focus({ preventScroll: true });
|
|
234
|
+
window.scrollTo(0, 0);
|
|
235
|
+
}, [routeKey]);
|
|
154
236
|
if (!userUid) {
|
|
155
237
|
return _jsx("div", { className: "min-h-screen" });
|
|
156
238
|
}
|
|
157
239
|
const apps = appRailItems(pluginNav);
|
|
158
240
|
// The header title: "settings" has no rail item, everything else is labelled by its own rail item.
|
|
159
241
|
const activeLabel = active === "settings" ? "Settings" : apps.find((app) => app.id === active)?.label;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
242
|
+
// A custom header (`Branding.headerHtml`) replaces the app's own title bar and the icon at the top of the rail: it is the top of the app, and the
|
|
243
|
+
// account menu moves into it. While it is still being parsed it already counts, so the frame's shape doesn't change under the user.
|
|
244
|
+
const customHeader = header !== null;
|
|
245
|
+
// Where the one account menu lives: the header's `{USER_MENU}`, else the footer's, else a small cell at the header's right end - never lost.
|
|
246
|
+
const menuInHeader = !!header?.hasUserMenu;
|
|
247
|
+
const menuInFooter = customHeader && !!header && !menuInHeader && !!footer?.hasUserMenu;
|
|
248
|
+
const renderUserMenu = (placement) => (_jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut,
|
|
249
|
+
// An impersonating administrator acts as the impersonated user, so the console link is hidden then.
|
|
250
|
+
showAdminLink: !!trusted && !impersonating, detectAdmin: !trusted && !impersonating, trustedRoles: trustedRoles, showSettingsLink: true, showNotificationSettings: true, onShowShortcuts: () => setShortcutsOpen(true), onShowNotifications: () => setHistoryOpen(true), unseenErrors: unseenErrors, placement: placement }));
|
|
251
|
+
return (_jsx(AppearanceProvider, { userUid: userUid, initial: appearance, lookUp: inFrame, children: _jsx(ShortcutProvider, { children: _jsxs(MailConnectionContext.Provider, { value: inFrame ? mail : null, children: [_jsx(GlobalShortcuts, { authServerUrl: authServerUrl, onToggleHelp: () => setShortcutsOpen((open) => !open) }), _jsx(ShortcutsDialog, { open: shortcutsOpen, onClose: () => setShortcutsOpen(false) }), _jsxs(UnlockPromptProvider, { children: [_jsx(UnlockBridge, {}), _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "rr-frame-bg min-h-screen flex flex-col", children: [!hideChrome && customHeader && (_jsx(FrameBrandingHeader, { parsed: header, userMenu: menuInHeader ? renderUserMenu("down") : undefined, fallbackMenu: header && !menuInHeader && !menuInFooter ? renderUserMenu("down") : undefined, appTitle: activeLabel })), impersonating && !hideChrome && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [!hideChrome && (_jsxs("nav", { "aria-label": "Apps", className: [
|
|
252
|
+
"hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center gap-1",
|
|
253
|
+
// Under a custom header the rail starts with the app icons; without one its own icon is flush with the top of the window.
|
|
254
|
+
customHeader ? "py-3" : "pb-3",
|
|
255
|
+
].join(" "), children: [!customHeader && (_jsx(RailIcon, { src: iconSrc })), apps.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
|
|
256
|
+
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
257
|
+
id === active
|
|
258
|
+
? "bg-primary/10 text-primary-dark"
|
|
259
|
+
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
260
|
+
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { ref: contentRef, id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), routeKey !== undefined && (_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
|
|
264
|
+
* is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
|
|
265
|
+
* mounted as the page is replaced - so this is only its children; everything it would have been given comes from the router
|
|
266
|
+
* frame instead (the props are the same for every page, and `active` is the route's).
|
|
267
|
+
*/
|
|
268
|
+
export default function AppShell(props) {
|
|
269
|
+
const inFrame = useInAppFrame();
|
|
270
|
+
return inFrame ? _jsx(_Fragment, { children: props.children }) : _jsx(AppChrome, { ...props });
|
|
166
271
|
}
|