@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
|
@@ -3,19 +3,28 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { createContext,
|
|
6
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
7
7
|
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
8
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
8
|
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
10
|
-
import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
11
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
10
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
13
11
|
import AppShell from "../../layout/AppShell.js";
|
|
12
|
+
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
14
13
|
import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
|
|
15
14
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
16
|
-
import { useCompose } from "../compose/ComposeContext.js";
|
|
15
|
+
import { prefetchComposeWindow, useCompose } from "../compose/ComposeContext.js";
|
|
17
16
|
import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
18
|
-
import { NO_LIVE_UPDATES
|
|
17
|
+
import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
|
|
18
|
+
import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
|
|
19
|
+
import { folderRows } from "../../../mail/folderTree.js";
|
|
20
|
+
import { badgeFor, badgeLabel, countOfFolder, inboxUnreadTotal, } from "../../../mail/folderCounts.js";
|
|
21
|
+
import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
|
|
22
|
+
import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
|
|
23
|
+
import OutboxBadge from "../OutboxBadge.js";
|
|
24
|
+
import { ariaKeyShortcuts, withHint } from "../../../keyboard/format.js";
|
|
25
|
+
import { SHORTCUTS } from "../../../keyboard/keymap.js";
|
|
26
|
+
import { useKeyEnvironment } from "../../../keyboard/ShortcutProvider.js";
|
|
27
|
+
import { useShortcut } from "../../../keyboard/useShortcut.js";
|
|
19
28
|
/** The well-known mail folder types an "aggregate" pseudo-folder can merge across every accessible
|
|
20
29
|
* mailbox - a fixed, small set: `outbox` is deliberately excluded (transient per-mailbox send-queue
|
|
21
30
|
* state, not a "merge across mailboxes" concept), and there's no aggregate concept for `calendar`/
|
|
@@ -25,11 +34,14 @@ export const AGGREGATE_FOLDER_TYPES = ["inbox", "sent_items", "drafts", "deleted
|
|
|
25
34
|
function isAggregateFolderType(value) {
|
|
26
35
|
return !!value && AGGREGATE_FOLDER_TYPES.includes(value);
|
|
27
36
|
}
|
|
37
|
+
const NO_TRACKER = { settle: () => undefined, revert: () => undefined };
|
|
28
38
|
const MailShellContext = createContext({
|
|
29
39
|
mailboxes: [],
|
|
30
40
|
mailboxFolders: [],
|
|
31
41
|
onFolderCreated: () => undefined,
|
|
42
|
+
noteFolderUids: () => undefined,
|
|
32
43
|
live: NO_LIVE_UPDATES,
|
|
44
|
+
trackMessageChange: () => NO_TRACKER,
|
|
33
45
|
});
|
|
34
46
|
/** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
|
|
35
47
|
export function useMailShell() {
|
|
@@ -44,34 +56,23 @@ const FOLDER_LABELS = {
|
|
|
44
56
|
archive: "Archive",
|
|
45
57
|
deleted_items: "Deleted Items",
|
|
46
58
|
};
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see
|
|
51
|
-
* `CalendarShell`/`ContactsShell`/`TasksShell`), not Mail — `listFolders()` returns every well-known
|
|
52
|
-
* folder for the mailbox regardless of which app owns it, so Mail's own folder tree must filter down
|
|
53
|
-
* to just the mail ones itself, or those other apps' folders leak into this sidebar.
|
|
54
|
-
*/
|
|
55
|
-
const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
|
|
56
|
-
function folderSortKey(folder) {
|
|
57
|
-
const idx = FOLDER_ORDER.indexOf(folder.type);
|
|
58
|
-
return idx === -1 ? FOLDER_ORDER.length : idx;
|
|
59
|
-
}
|
|
60
|
-
function sortedFoldersOf(folders) {
|
|
61
|
-
return [...folders].sort((a, b) => folderSortKey(a) - folderSortKey(b) || a.name.localeCompare(b.name));
|
|
62
|
-
}
|
|
63
|
-
/** A folder's unread count: the refreshed one when live updates have read it since load, else the one it was listed with. */
|
|
64
|
-
function unreadOf(folder, unreadCounts) {
|
|
65
|
-
return unreadCounts[folder.uid] ?? folder.unreadCount;
|
|
66
|
-
}
|
|
67
|
-
function aggregateUnreadCount(mailboxFolders, type, unreadCounts) {
|
|
68
|
-
return mailboxFolders.reduce((total, mf) => {
|
|
59
|
+
/** The badge an "All Mailboxes" entry shows: the folders of that type, summed across every mailbox, under the same rules as a single folder's. */
|
|
60
|
+
function aggregateBadge(mailboxFolders, type, counts) {
|
|
61
|
+
const sum = mailboxFolders.reduce((total, mf) => {
|
|
69
62
|
const folder = mf.folders.find((f) => f.type === type);
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const count = folder ? countOfFolder(folder, counts) : { unread: 0, total: 0 };
|
|
64
|
+
return { unread: total.unread + count.unread, total: total.total + count.total };
|
|
65
|
+
}, { unread: 0, total: 0 });
|
|
66
|
+
return badgeFor(type, sum);
|
|
67
|
+
}
|
|
68
|
+
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
69
|
+
function FolderBadgeChip({ badge }) {
|
|
70
|
+
return (_jsxs("span", { className: [
|
|
71
|
+
"text-xs rounded-pill py-0.5 px-1.5",
|
|
72
|
+
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
73
|
+
].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
export const MAILBOX_LIST_LIMIT = 100;
|
|
75
|
+
export { MAILBOX_LIST_LIMIT };
|
|
75
76
|
/**
|
|
76
77
|
* A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
|
|
77
78
|
* `ComposeProvider` correctly: that provider is rendered *inside* the `AppShell` that `MailShell` itself
|
|
@@ -80,12 +81,25 @@ export const MAILBOX_LIST_LIMIT = 100;
|
|
|
80
81
|
* its own descendants creates. This button, rendered as part of `AppShell`'s `children`, sits correctly
|
|
81
82
|
* inside that subtree.
|
|
82
83
|
*
|
|
83
|
-
* Opens with
|
|
84
|
-
*
|
|
84
|
+
* Opens with the caller's own mailbox (the shell has already listed them), so the window can start on its
|
|
85
|
+
* signature, draft and encryption lookups straight away instead of first asking which mailbox that is; the
|
|
86
|
+
* compose window's own From field is where the sender is chosen. Fetches the window's code as the pointer or
|
|
87
|
+
* keyboard reaches the button, so the click finds it already here.
|
|
85
88
|
*/
|
|
86
|
-
function ComposeButton() {
|
|
89
|
+
function ComposeButton({ mailboxUid }) {
|
|
87
90
|
const { openCompose } = useCompose();
|
|
88
|
-
|
|
91
|
+
const env = useKeyEnvironment();
|
|
92
|
+
return (_jsx("button", { type: "button", title: withHint("Compose", SHORTCUTS.mail.create, env), "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.mail.create, env), onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow, className: "block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark", children: "Compose" }));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Mail's keyboard shortcuts that don't belong to a list or a message: "New message". A component of its own for the reason `ComposeButton`
|
|
96
|
+
* is - `useCompose()` only resolves below `ComposeProvider`, i.e. inside the `AppShell` this shell renders - and mounted once, whatever number
|
|
97
|
+
* of Compose buttons (the sidebar, the folders drawer) are on screen. Opens the same window, for the same mailbox, as the button.
|
|
98
|
+
*/
|
|
99
|
+
function MailShortcuts({ mailboxUid }) {
|
|
100
|
+
const { openCompose } = useCompose();
|
|
101
|
+
useShortcut(SHORTCUTS.mail.create, () => openCompose({ mailboxUid }));
|
|
102
|
+
return null;
|
|
89
103
|
}
|
|
90
104
|
/**
|
|
91
105
|
* Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
|
|
@@ -103,36 +117,35 @@ function ComposeButton() {
|
|
|
103
117
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
104
118
|
* of folders" requirement.
|
|
105
119
|
*/
|
|
106
|
-
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
120
|
+
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
121
|
+
// The mailboxes, their folders and the live connection come from the persistent app frame when there is one (`AppChrome` owns them, so
|
|
122
|
+
// they survive Mail -> Calendar -> Mail and the push socket, pop-ups, counters and tab title work in every app). A page rendered
|
|
123
|
+
// outside the frame - a test, a plugin page - has no frame to ask, so this shell runs the same hook for itself, and renders the pop-ups.
|
|
124
|
+
const navigate = useNavigate();
|
|
125
|
+
const hosted = useContext(MailConnectionContext);
|
|
126
|
+
const own = useMailConnection({ userUid, enabled: !hosted, open: navigate });
|
|
127
|
+
const { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, outbox: outboxStatuses } = hosted ?? own;
|
|
128
|
+
// The messages this tab is still handing to the server: the Outbox pill counts them at once.
|
|
129
|
+
const pendingSends = usePendingSends();
|
|
112
130
|
const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
|
|
113
131
|
const [requestedFolderUid, setRequestedFolderUid] = useState(null);
|
|
114
132
|
const [requestedAggregateType, setRequestedAggregateType] = useState(null);
|
|
115
133
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
134
|
+
// The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
|
|
135
|
+
// clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
|
|
136
|
+
// in an effect, never during the first render, so the server render and the hydrating render agree.
|
|
137
|
+
const search = useLocationSearch();
|
|
116
138
|
useEffect(() => {
|
|
117
|
-
const params = new URLSearchParams(
|
|
139
|
+
const params = new URLSearchParams(search);
|
|
118
140
|
setRequestedMailboxUid(params.get("mailboxUid"));
|
|
119
141
|
setRequestedFolderUid(params.get("folderUid"));
|
|
120
142
|
setRequestedAggregateType(params.get("aggregate"));
|
|
121
|
-
}, []);
|
|
143
|
+
}, [search]);
|
|
144
|
+
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
145
|
+
// with the page - is closed here.
|
|
122
146
|
useEffect(() => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
listMailboxes({ limit: MAILBOX_LIST_LIMIT })
|
|
127
|
-
.then((result) => {
|
|
128
|
-
setMailboxes(result);
|
|
129
|
-
setStatus("ready");
|
|
130
|
-
})
|
|
131
|
-
.catch((err) => {
|
|
132
|
-
setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
|
|
133
|
-
setStatus("error");
|
|
134
|
-
});
|
|
135
|
-
}, [userUid]);
|
|
147
|
+
setDrawerOpen(false);
|
|
148
|
+
}, [search]);
|
|
136
149
|
const aggregateFolderType = isAggregateFolderType(requestedAggregateType)
|
|
137
150
|
? requestedAggregateType
|
|
138
151
|
: undefined;
|
|
@@ -140,29 +153,6 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
140
153
|
? undefined
|
|
141
154
|
: (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
142
155
|
mailboxes[0]?.uid;
|
|
143
|
-
// Fans out one listFolders() call per accessible mailbox in parallel - each call catches its own
|
|
144
|
-
// failure into an MailboxFolders.error rather than letting Promise.all reject, so one mailbox's fetch
|
|
145
|
-
// failure renders that section's own inline Alert instead of blanking out every other mailbox's
|
|
146
|
-
// folder tree.
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (mailboxes.length === 0) {
|
|
149
|
-
// Deliberately leaves foldersLoading as-is: this also runs once before listMailboxes() has
|
|
150
|
-
// resolved, and clearing it here would briefly render an empty sidebar the moment mailboxes
|
|
151
|
-
// arrive, before their folders do. A genuinely mailbox-less caller gets MailboxProvisioning.
|
|
152
|
-
setMailboxFolders([]);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
setFoldersLoading(true);
|
|
156
|
-
void Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid)
|
|
157
|
-
.then((result) => ({ mailbox, folders: result.filter((f) => MAIL_FOLDER_TYPES.has(f.type)) }))
|
|
158
|
-
.catch((err) => ({
|
|
159
|
-
mailbox,
|
|
160
|
-
folders: [],
|
|
161
|
-
error: err instanceof ApiRequestError ? err.message : "Could not load folders.",
|
|
162
|
-
}))))
|
|
163
|
-
.then(setMailboxFolders)
|
|
164
|
-
.finally(() => setFoldersLoading(false));
|
|
165
|
-
}, [mailboxes]);
|
|
166
156
|
const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
|
|
167
157
|
const folderUid = aggregateFolderType
|
|
168
158
|
? undefined
|
|
@@ -175,23 +165,11 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
175
165
|
// mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
|
|
176
166
|
const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
|
|
177
167
|
const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
setMailboxFolders((prev) => prev.map((entry) =>
|
|
185
|
-
// Not one already there: another client's create event can arrive after (or twice, or alongside) our own.
|
|
186
|
-
entry.mailbox.uid === folder.mailboxUid && !entry.folders.some((existing) => existing.uid === folder.uid)
|
|
187
|
-
? { ...entry, folders: [...entry.folders, folder] }
|
|
188
|
-
: entry));
|
|
189
|
-
}, []);
|
|
190
|
-
// New mail without a reload: push events for every folder, plus a safety-net poll. Held in state beside `mailboxFolders`
|
|
191
|
-
// rather than written into it, so a refreshed unread count never looks like a change of folders to the list on screen
|
|
192
|
-
// (which reloads, and forgets its selection, whenever the folders do).
|
|
193
|
-
const { live, unreadCounts } = useMailLiveUpdates({ userUid, mailboxes, mailboxFolders, onFolderCreated });
|
|
194
|
-
const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, live }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, live]);
|
|
168
|
+
const counts = folderCounts.counts;
|
|
169
|
+
const trackMessageChange = folderCounts.track;
|
|
170
|
+
// `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
|
|
171
|
+
useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
|
|
172
|
+
const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange]);
|
|
195
173
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
|
|
196
174
|
// mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
|
|
197
175
|
// `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
|
|
@@ -212,24 +190,32 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
212
190
|
// A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
|
|
213
191
|
// `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
|
|
214
192
|
// unmounted).
|
|
215
|
-
const sidebarContent = () => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, {}) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5", children: "All Mailboxes" }), _jsx("div", { className: "flex flex-col gap-0.5", children: AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
216
|
-
const
|
|
193
|
+
const sidebarContent = () => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5", children: "All Mailboxes" }), _jsx("div", { className: "flex flex-col gap-0.5", children: AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
194
|
+
const badge = aggregateBadge(mailboxFolders, type, counts);
|
|
217
195
|
return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, className: [
|
|
218
196
|
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
219
197
|
aggregateFolderType === type
|
|
220
198
|
? "bg-primary/10 text-primary-dark font-semibold"
|
|
221
199
|
: "text-text hover:bg-surface-alt",
|
|
222
|
-
].join(" "), children: [_jsx("span", { children: FOLDER_LABELS[type] }),
|
|
223
|
-
}) })] })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsxs("div", { children: [_jsxs("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children:
|
|
224
|
-
|
|
200
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
|
|
201
|
+
}) })] })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsxs("div", { children: [_jsxs("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
202
|
+
if (row.kind === "placeholder") {
|
|
203
|
+
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
204
|
+
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
205
|
+
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
206
|
+
return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
|
|
207
|
+
}
|
|
208
|
+
const folder = row.folder;
|
|
209
|
+
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
225
210
|
return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, className: [
|
|
226
211
|
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
227
212
|
folder.uid === folderUid
|
|
228
213
|
? "bg-primary/10 text-primary-dark font-semibold"
|
|
229
214
|
: "text-text hover:bg-surface-alt",
|
|
230
|
-
].join(" "), children: [_jsx("span", {
|
|
215
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
|
|
216
|
+
(folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
|
|
231
217
|
}) })] }, mailbox.uid)))] }))] }));
|
|
232
|
-
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent() }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", children: _jsx("div", { className: "flex flex-col", children: sidebarContent() }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsx("button", { type: "button", className: "md: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 folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
218
|
+
inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent() }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", children: _jsx("div", { className: "flex flex-col", children: sidebarContent() }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsx("button", { type: "button", className: "md: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 folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
233
219
|
}
|
|
234
220
|
// Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
|
|
235
221
|
// resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
|
|
@@ -251,5 +237,5 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
251
237
|
canProvision: !impersonating && activeMailbox?.ownerUserUid === userUid, children: [_jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [],
|
|
252
238
|
// A full page may be truncated - pruning against it would delete indexes of accessible mailboxes
|
|
253
239
|
// beyond the limit, so it's only trusted as the complete list when it came back short.
|
|
254
|
-
accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner })] }));
|
|
240
|
+
accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner })] }));
|
|
255
241
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface EncryptedBodyProps {
|
|
3
|
+
/** This device has no unlocked key session for the message's mailbox: unlocking could help. */
|
|
4
|
+
locked: boolean;
|
|
5
|
+
/** Why the message couldn't be decrypted when the keys were available (shown for the failed state). */
|
|
6
|
+
reason?: string;
|
|
7
|
+
/** Opens the app's unlock prompt (`useUnlockPrompt().requestUnlock`). Only used - and only offered - when `locked`. */
|
|
8
|
+
onUnlock: () => void;
|
|
9
|
+
/** An unlock is being asked for: the button waits. */
|
|
10
|
+
unlocking?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What an encrypted message's body is until (and unless) it can be read, drawn as card content in the app's own tokens: on the theme's opaque surface, so
|
|
14
|
+
* it stays legible over a translucent card and a background photo.
|
|
15
|
+
*
|
|
16
|
+
* **Locked** - the keys are not unlocked: a lock, "This message is encrypted", a hint and a primary Unlock button, which opens the same prompt compose's
|
|
17
|
+
* "Unlock to sign or encrypt" does. When it succeeds the pane decrypts and shows the message in place.
|
|
18
|
+
* **Cannot be decrypted** - the keys are unlocked and it still can't be opened (no key for this mailbox, not a recipient, damaged, unsupported): the reason,
|
|
19
|
+
* and no button - unlocking again wouldn't change it.
|
|
20
|
+
*/
|
|
21
|
+
export default function EncryptedBody({ locked, reason, onUnlock, unlocking }: EncryptedBodyProps): React.JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineExclamationTriangle, HiOutlineLockClosed } from "react-icons/hi2";
|
|
3
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
4
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
5
|
+
import { useResolvedTheme } from "../../../appearance/resolvedTheme.js";
|
|
6
|
+
import { formatColour } from "./color.js";
|
|
7
|
+
import { useThemeSurface } from "./themeSurface.js";
|
|
8
|
+
/**
|
|
9
|
+
* What an encrypted message's body is until (and unless) it can be read, drawn as card content in the app's own tokens: on the theme's opaque surface, so
|
|
10
|
+
* it stays legible over a translucent card and a background photo.
|
|
11
|
+
*
|
|
12
|
+
* **Locked** - the keys are not unlocked: a lock, "This message is encrypted", a hint and a primary Unlock button, which opens the same prompt compose's
|
|
13
|
+
* "Unlock to sign or encrypt" does. When it succeeds the pane decrypts and shows the message in place.
|
|
14
|
+
* **Cannot be decrypted** - the keys are unlocked and it still can't be opened (no key for this mailbox, not a recipient, damaged, unsupported): the reason,
|
|
15
|
+
* and no button - unlocking again wouldn't change it.
|
|
16
|
+
*/
|
|
17
|
+
export default function EncryptedBody({ locked, reason, onUnlock, unlocking = false }) {
|
|
18
|
+
const theme = useResolvedTheme();
|
|
19
|
+
const surface = useThemeSurface(theme);
|
|
20
|
+
const Icon = locked ? HiOutlineLockClosed : HiOutlineExclamationTriangle;
|
|
21
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-md border border-border px-4 py-8 text-center", style: { backgroundColor: formatColour(surface.background) }, children: [_jsx("span", { "aria-hidden": "true", className: ["inline-flex h-12 w-12 items-center justify-center rounded-full", locked ? "bg-primary/10 text-primary-dark" : "bg-warning/15 text-text"].join(" "), children: _jsx(Icon, { size: 24 }) }), _jsx("p", { className: "text-base font-semibold text-text", children: locked ? "This message is encrypted" : "This message can’t be decrypted" }), locked ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-text-muted", children: "Unlock your keys to read it" }), _jsx(Button, { type: "button", className: "!w-auto mt-1", "aria-label": "Unlock to view this message", loading: unlocking, disabled: unlocking, onClick: onUnlock, children: "Unlock" })] })) : (reason && (_jsx("div", { className: "mt-1 max-w-prose text-left", children: _jsx(Alert, { children: reason }) })))] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** The subject an encrypted message carries on the server (RFC 9788's placeholder): the real one is inside the ciphertext. */
|
|
3
|
+
export declare const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
|
|
4
|
+
/**
|
|
5
|
+
* What a list row's preview line says for an encrypted message nothing has been decrypted of yet: a small lock and "Encrypted message". The server never
|
|
6
|
+
* derives a preview for encrypted mail (it has no plaintext), so the line would otherwise be empty.
|
|
7
|
+
*/
|
|
8
|
+
export declare function EncryptedPreview(): React.JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a conversation row stands for an encrypted latest message. A conversation summary carries no flag for it, but an encrypted message has no preview
|
|
11
|
+
* and the placeholder subject - which a plaintext message with an empty body does not.
|
|
12
|
+
*/
|
|
13
|
+
export declare function conversationLooksEncrypted(conversation: {
|
|
14
|
+
subject: string;
|
|
15
|
+
latestPreview: string;
|
|
16
|
+
}): boolean;
|
|
17
|
+
/** The subject to show for a message's own: what an encrypted message says is the placeholder, which nobody should have to read as a title. */
|
|
18
|
+
export declare function displaySubject(subject: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineLockClosed } from "react-icons/hi2";
|
|
3
|
+
/** The subject an encrypted message carries on the server (RFC 9788's placeholder): the real one is inside the ciphertext. */
|
|
4
|
+
export const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
|
|
5
|
+
/**
|
|
6
|
+
* What a list row's preview line says for an encrypted message nothing has been decrypted of yet: a small lock and "Encrypted message". The server never
|
|
7
|
+
* derives a preview for encrypted mail (it has no plaintext), so the line would otherwise be empty.
|
|
8
|
+
*/
|
|
9
|
+
export function EncryptedPreview() {
|
|
10
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true", className: "shrink-0" }), "Encrypted message"] }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether a conversation row stands for an encrypted latest message. A conversation summary carries no flag for it, but an encrypted message has no preview
|
|
14
|
+
* and the placeholder subject - which a plaintext message with an empty body does not.
|
|
15
|
+
*/
|
|
16
|
+
export function conversationLooksEncrypted(conversation) {
|
|
17
|
+
return conversation.latestPreview === "" && conversation.subject === ENCRYPTED_SUBJECT_PLACEHOLDER;
|
|
18
|
+
}
|
|
19
|
+
/** The subject to show for a message's own: what an encrypted message says is the placeholder, which nobody should have to read as a title. */
|
|
20
|
+
export function displaySubject(subject) {
|
|
21
|
+
return subject === ENCRYPTED_SUBJECT_PLACEHOLDER ? "Encrypted message" : subject;
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Attachment } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
import type { MimeAttachment } from "@rapidmx/react-shared/crypto/mime.js";
|
|
4
|
+
import { BodyContent } from "./bodyContent.js";
|
|
5
|
+
export interface MessageBodyProps {
|
|
6
|
+
messageUid: string;
|
|
7
|
+
/** Part of what a remembered body is keyed on, so an edited message is fetched again. */
|
|
8
|
+
messageVersion: number;
|
|
9
|
+
/** The accessible name of the body. */
|
|
10
|
+
title: string;
|
|
11
|
+
/** A body the caller already has - decrypted or verified content. Shown instead of asking the server. */
|
|
12
|
+
content?: BodyContent;
|
|
13
|
+
/** The message's server-side attachments, for resolving its inline `cid:` images. */
|
|
14
|
+
attachments?: Attachment[];
|
|
15
|
+
/** The parts inside a signed or encrypted message, for resolving its inline images from the decrypted content. */
|
|
16
|
+
inlineParts?: MimeAttachment[];
|
|
17
|
+
/** Show the message as its author wrote it: nothing adapted to the theme. */
|
|
18
|
+
original?: boolean;
|
|
19
|
+
/** Says whether "view original" would change anything, once that is known: never for plain text, a message with its own dark styles, or a
|
|
20
|
+
* message the theme leaves as it is. */
|
|
21
|
+
onAdaptable?: (adaptable: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
/** Placeholder lines shown where the body will be, in the body's own place, until it has arrived and been laid out. */
|
|
24
|
+
export declare function BodySkeleton(): React.JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* The body of one message, in the card's own place: the sanitized HTML in a frame that is exactly as tall as its content (following it as
|
|
27
|
+
* images load and the width changes), or plain text. See `frameDocument.ts` for the isolation and `themeAdaptation.ts` for how a message
|
|
28
|
+
* takes the app's theme.
|
|
29
|
+
*
|
|
30
|
+
* Nothing waits on the network to lay out: the skeleton is here at once, and the frame's height is only ever the height of what is in it.
|
|
31
|
+
*/
|
|
32
|
+
export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original, onAdaptable }: MessageBodyProps): React.JSX.Element;
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { ApiRequestError, apiUrl } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
|
+
import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
11
|
+
import { useResolvedTheme } from "../../../appearance/resolvedTheme.js";
|
|
12
|
+
import { attachmentSource, cachedBodyContent, fetchBodyContent, makeCidResolver } from "./bodyContent.js";
|
|
13
|
+
import { prepareBodyHtml } from "./bodyHtml.js";
|
|
14
|
+
import { formatColour } from "./color.js";
|
|
15
|
+
import { controlFrame } from "./frameControl.js";
|
|
16
|
+
import { FRAME_SANDBOX, buildFrameDocument } from "./frameDocument.js";
|
|
17
|
+
import { surfaceKey, useThemeSurface } from "./themeSurface.js";
|
|
18
|
+
/** Placeholder lines shown where the body will be, in the body's own place, until it has arrived and been laid out. */
|
|
19
|
+
export function BodySkeleton() {
|
|
20
|
+
return (_jsxs("div", { role: "status", "aria-busy": "true", className: "flex flex-col gap-2.5 py-1", children: [_jsx("span", { className: "sr-only", children: "Loading the message" }), _jsx(Skeleton, { width: "w-11/12" }), _jsx(Skeleton, { width: "w-4/5" }), _jsx(Skeleton, { width: "w-9/12" }), _jsx(Skeleton, { width: "w-1/2" })] }));
|
|
21
|
+
}
|
|
22
|
+
/** A short, stable fingerprint of a document's text (djb2), to tell one loaded document from the next. */
|
|
23
|
+
function fingerprint(text) {
|
|
24
|
+
let hash = 5381;
|
|
25
|
+
for (let i = 0; i < text.length; i++) {
|
|
26
|
+
hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0;
|
|
27
|
+
}
|
|
28
|
+
return (hash >>> 0).toString(36);
|
|
29
|
+
}
|
|
30
|
+
/** The background of a message shown as authored: white, the canvas an HTML message is written for. */
|
|
31
|
+
const AUTHORED_CANVAS = "rgb(255, 255, 255)";
|
|
32
|
+
/**
|
|
33
|
+
* The body of one message, in the card's own place: the sanitized HTML in a frame that is exactly as tall as its content (following it as
|
|
34
|
+
* images load and the width changes), or plain text. See `frameDocument.ts` for the isolation and `themeAdaptation.ts` for how a message
|
|
35
|
+
* takes the app's theme.
|
|
36
|
+
*
|
|
37
|
+
* Nothing waits on the network to lay out: the skeleton is here at once, and the frame's height is only ever the height of what is in it.
|
|
38
|
+
*/
|
|
39
|
+
export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original = false, onAdaptable }) {
|
|
40
|
+
const [remote, setRemote] = useState(() => {
|
|
41
|
+
const cached = cachedBodyContent(messageUid, messageVersion);
|
|
42
|
+
return cached ? { status: "ready", content: cached } : { status: "loading" };
|
|
43
|
+
});
|
|
44
|
+
const [attempt, setAttempt] = useState(0);
|
|
45
|
+
const supplied = content !== undefined;
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (supplied) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const cached = cachedBodyContent(messageUid, messageVersion);
|
|
51
|
+
if (cached) {
|
|
52
|
+
setRemote({ status: "ready", content: cached });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
setRemote({ status: "loading" });
|
|
57
|
+
fetchBodyContent(messageUid, messageVersion, controller.signal).then((loaded) => setRemote({ status: "ready", content: loaded }), (err) => {
|
|
58
|
+
if (!controller.signal.aborted) {
|
|
59
|
+
setRemote({ status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message." });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return () => controller.abort();
|
|
63
|
+
}, [messageUid, messageVersion, supplied, attempt]);
|
|
64
|
+
const shown = content ? { status: "ready", content } : remote;
|
|
65
|
+
if (shown.status === "loading") {
|
|
66
|
+
return _jsx(BodySkeleton, {});
|
|
67
|
+
}
|
|
68
|
+
if (shown.status === "error") {
|
|
69
|
+
return (_jsxs("div", { className: "flex flex-col items-start gap-2", children: [_jsx(Alert, { children: shown.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setAttempt((n) => n + 1), children: "Try again" })] }));
|
|
70
|
+
}
|
|
71
|
+
if (shown.content.kind === "text") {
|
|
72
|
+
// Rendered as text - React escapes it - never as markup, in the theme's own colours.
|
|
73
|
+
return _jsx("pre", { "aria-label": title, className: "m-0 text-sm font-sans whitespace-pre-wrap break-words text-text", children: shown.content.text });
|
|
74
|
+
}
|
|
75
|
+
return (_jsx(HtmlBody, { messageUid: messageUid, title: title, html: shown.content.html, remoteAvailable: !supplied, attachments: attachments, inlineParts: inlineParts, original: original, onAdaptable: onAdaptable }, messageUid));
|
|
76
|
+
}
|
|
77
|
+
function HtmlBody({ messageUid, title, html, remoteAvailable, attachments, inlineParts, original, onAdaptable, }) {
|
|
78
|
+
const theme = useResolvedTheme();
|
|
79
|
+
const surface = useThemeSurface(theme);
|
|
80
|
+
// Keyed on what the resolver reads rather than on the arrays, which a caller may rebuild on every render.
|
|
81
|
+
const inlineKey = [...(attachments ?? []).map((a) => `${a.uid}:${a.contentId ?? ""}`), inlineParts ? `parts:${inlineParts.length}` : ""].join("|");
|
|
82
|
+
const resolveCid = useMemo(() => makeCidResolver(attachments, inlineParts), [inlineKey]);
|
|
83
|
+
const prepared = useMemo(() => prepareBodyHtml(html, { resolveCid }), [html, resolveCid]);
|
|
84
|
+
const mode = prepared.declaresDarkSupport ? "native" : original ? "original" : "adapt";
|
|
85
|
+
const surfaceCss = formatColour(surface.background);
|
|
86
|
+
const srcDoc = useMemo(() => {
|
|
87
|
+
// The frame may load images from this server's attachment URLs only when an inline image was actually resolved to one.
|
|
88
|
+
const source = prepared.inlineImages > 0 ? attachmentSource() : undefined;
|
|
89
|
+
return buildFrameDocument(prepared.html, {
|
|
90
|
+
mode,
|
|
91
|
+
scheme: theme,
|
|
92
|
+
surface: surfaceCss,
|
|
93
|
+
text: formatColour(surface.text),
|
|
94
|
+
attachmentSource: source && prepared.html.includes(source) ? source : undefined,
|
|
95
|
+
});
|
|
96
|
+
}, [prepared, mode, theme, surfaceCss, surface.text]);
|
|
97
|
+
// A different body, surface or mode is a different document: the adaptation pass edits the document it ran on, so it is run on a fresh
|
|
98
|
+
// one, and the old one's readiness is not the new one's.
|
|
99
|
+
const frameKey = useMemo(() => `${mode}:${surfaceKey(surface)}:${fingerprint(srcDoc)}`, [mode, surface, srcDoc]);
|
|
100
|
+
const [height, setHeight] = useState(undefined);
|
|
101
|
+
const [readyFor, setReadyFor] = useState(null);
|
|
102
|
+
const [canvasAuthored, setCanvasAuthored] = useState(false);
|
|
103
|
+
const dispose = useRef(undefined);
|
|
104
|
+
useEffect(() => () => dispose.current?.(), []);
|
|
105
|
+
const reportAdaptable = useRef(onAdaptable);
|
|
106
|
+
reportAdaptable.current = onAdaptable;
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (mode === "native") {
|
|
109
|
+
reportAdaptable.current?.(false);
|
|
110
|
+
}
|
|
111
|
+
}, [mode]);
|
|
112
|
+
if (prepared.status === "too_large") {
|
|
113
|
+
return (_jsxs(Alert, { children: ["This message is too large to show here.", " ", remoteAvailable && (_jsx("a", { href: apiUrl(`/mail/messages/${encodeURIComponent(messageUid)}/content`), target: "_blank", rel: "noopener noreferrer", className: "font-medium underline", children: "Open it in its own tab" }))] }));
|
|
114
|
+
}
|
|
115
|
+
const ready = readyFor === frameKey;
|
|
116
|
+
const canvas = mode === "original" ? AUTHORED_CANVAS : surfaceCss;
|
|
117
|
+
const framed = mode !== "adapt" || canvasAuthored;
|
|
118
|
+
return (_jsxs("div", { className: "relative rounded-md overflow-hidden", style: { backgroundColor: canvas }, children: [!ready && _jsx(BodySkeleton, {}), _jsx("iframe", { title: title, srcDoc: srcDoc, sandbox: FRAME_SANDBOX, scrolling: "no", onLoad: (event) => {
|
|
119
|
+
dispose.current?.();
|
|
120
|
+
dispose.current = controlFrame(event.currentTarget, {
|
|
121
|
+
surface: mode === "adapt" ? surface : undefined,
|
|
122
|
+
onHeight: setHeight,
|
|
123
|
+
onReady: (adaptation) => {
|
|
124
|
+
setReadyFor(frameKey);
|
|
125
|
+
if (adaptation) {
|
|
126
|
+
setCanvasAuthored(adaptation.authoredCanvas);
|
|
127
|
+
reportAdaptable.current?.(adaptation.material);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}, className: [
|
|
132
|
+
"block w-full border-0 rounded-md",
|
|
133
|
+
ready ? "" : "absolute inset-x-0 top-0 invisible",
|
|
134
|
+
framed && ready ? "ring-1 ring-border" : "",
|
|
135
|
+
].join(" "), style: { height: height ?? 0 } }, frameKey)] }));
|
|
136
|
+
}
|