@rapidmx/web-client 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +373 -88
- package/apps/admin/_layout.tsx +8 -2
- package/apps/admin/index.tsx +7 -1
- package/apps/admin/ingest-queue/index.tsx +2 -1
- package/apps/admin/mailboxes/[uid].tsx +23 -10
- package/apps/admin/quarantine/index.tsx +2 -1
- package/apps/admin/signing-certificates/index.tsx +343 -0
- package/apps/escrow/_layout.tsx +8 -2
- package/apps/shared/appearance/AppearanceForm.tsx +392 -0
- package/apps/shared/appearance/AppearanceHead.tsx +49 -0
- package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
- package/apps/shared/appearance/ColorField.tsx +107 -0
- package/apps/shared/appearance/appearanceCache.ts +106 -0
- package/apps/shared/appearance/appearanceContext.ts +77 -0
- package/apps/shared/appearance/bootScript.ts +35 -0
- package/apps/shared/appearance/color.ts +169 -0
- package/apps/shared/appearance/photo.ts +88 -0
- package/apps/shared/appearance/resolvedTheme.ts +67 -0
- package/apps/shared/appearance/theme.ts +355 -0
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
- package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
- package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
- package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactForm.tsx +11 -5
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
- package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
- package/apps/shared/components/layout/AppShell.tsx +477 -288
- package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
- package/apps/shared/components/layout/RailIcon.tsx +100 -0
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
- package/apps/shared/components/layout/UserMenu.tsx +372 -185
- package/apps/shared/components/mail/ConversationList.tsx +271 -262
- package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
- package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
- package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
- package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
- package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
- package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
- package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
- package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
- package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
- package/apps/shared/components/mail/reading/color.ts +245 -0
- package/apps/shared/components/mail/reading/frameControl.ts +142 -0
- package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
- package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
- package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
- package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
- package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
- package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
- package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +308 -0
- package/apps/shared/mail/folderTree.ts +143 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
- package/apps/shared/mail/outbox/composeBridge.ts +69 -0
- package/apps/shared/mail/outbox/outboxState.ts +88 -0
- package/apps/shared/mail/outbox/pendingSends.ts +146 -0
- package/apps/shared/mail/outbox/sendDecision.ts +157 -0
- package/apps/shared/mail/outbox/sendJob.ts +427 -0
- package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
- package/apps/shared/mail/outbox/sendState.ts +35 -0
- package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
- package/apps/shared/mail/useMailConnection.ts +203 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +178 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/notifications/NotificationCenter.tsx +215 -0
- package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
- package/apps/shared/notifications/apiErrors.ts +98 -0
- package/apps/shared/notifications/headerOffset.ts +35 -0
- package/apps/shared/notifications/pushStatus.ts +56 -0
- package/apps/shared/notifications/store.ts +549 -0
- package/apps/shared/notifications/systemErrors.ts +63 -0
- package/apps/shared/notifications/useNotifications.ts +41 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/signing/enrollmentStorage.ts +33 -0
- package/apps/shared/signing/enrollmentTracker.ts +385 -0
- package/apps/shared/signing/enrollmentView.ts +251 -0
- package/apps/shared/signing/signingInfo.ts +58 -0
- package/apps/shared/signing/useNow.ts +19 -0
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
- package/apps/shared/styles/app.css +108 -10
- package/apps/www/_layout.tsx +8 -2
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +36 -0
- package/apps/www/calendar/index.tsx +64 -33
- package/apps/www/contacts/[uid].tsx +109 -107
- package/apps/www/contacts/index.tsx +62 -42
- package/apps/www/index.tsx +2291 -1917
- package/apps/www/messages/[uid].tsx +108 -101
- package/apps/www/settings/appearance/index.tsx +21 -0
- package/apps/www/settings/auto-reply/index.tsx +7 -8
- package/apps/www/settings/encryption/index.tsx +1287 -1249
- package/apps/www/settings/filters/[uid].tsx +7 -2
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +140 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +7 -9
- package/apps/www/settings/read-receipts/index.tsx +7 -8
- package/apps/www/settings/sharing/index.tsx +256 -274
- package/apps/www/settings/signatures/[uid].tsx +172 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +135 -129
- package/apps/www/tasks/index.tsx +29 -18
- package/dist/apps/admin/_layout.d.ts +5 -1
- package/dist/apps/admin/_layout.js +3 -2
- package/dist/apps/admin/index.js +3 -2
- package/dist/apps/admin/ingest-queue/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +9 -6
- package/dist/apps/admin/quarantine/index.js +2 -1
- package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
- package/dist/apps/admin/signing-certificates/index.js +161 -0
- package/dist/apps/escrow/_layout.d.ts +5 -1
- package/dist/apps/escrow/_layout.js +3 -2
- package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
- package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
- package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
- package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
- package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
- package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
- package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
- package/dist/apps/shared/appearance/ColorField.js +40 -0
- package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceCache.js +80 -0
- package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceContext.js +29 -0
- package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
- package/dist/apps/shared/appearance/bootScript.js +33 -0
- package/dist/apps/shared/appearance/color.d.ts +65 -0
- package/dist/apps/shared/appearance/color.js +132 -0
- package/dist/apps/shared/appearance/photo.d.ts +17 -0
- package/dist/apps/shared/appearance/photo.js +79 -0
- package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
- package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
- package/dist/apps/shared/appearance/theme.d.ts +111 -0
- package/dist/apps/shared/appearance/theme.js +251 -0
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
- package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
- package/dist/apps/shared/components/layout/AppShell.js +122 -17
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
- package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
- package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
- package/dist/apps/shared/components/layout/RailIcon.js +85 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
- package/dist/apps/shared/components/layout/UserMenu.js +88 -7
- package/dist/apps/shared/components/mail/ConversationList.js +5 -11
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
- package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
- package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
- package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
- package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
- package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
- package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
- package/dist/apps/shared/components/mail/reading/color.js +201 -0
- package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
- package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +218 -0
- package/dist/apps/shared/mail/folderTree.d.ts +55 -0
- package/dist/apps/shared/mail/folderTree.js +116 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
- package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
- package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
- package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
- package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
- package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
- package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
- package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
- package/dist/apps/shared/mail/outbox/sendState.js +8 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
- package/dist/apps/shared/mail/useMailConnection.js +138 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
- package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
- package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
- package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
- package/dist/apps/shared/notifications/apiErrors.js +86 -0
- package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
- package/dist/apps/shared/notifications/headerOffset.js +33 -0
- package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
- package/dist/apps/shared/notifications/pushStatus.js +54 -0
- package/dist/apps/shared/notifications/store.d.ts +136 -0
- package/dist/apps/shared/notifications/store.js +416 -0
- package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
- package/dist/apps/shared/notifications/systemErrors.js +58 -0
- package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
- package/dist/apps/shared/notifications/useNotifications.js +18 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
- package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
- package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
- package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
- package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
- package/dist/apps/shared/signing/enrollmentView.js +214 -0
- package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
- package/dist/apps/shared/signing/signingInfo.js +48 -0
- package/dist/apps/shared/signing/useNow.d.ts +2 -0
- package/dist/apps/shared/signing/useNow.js +18 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
- package/dist/apps/shared/styles/app.css +108 -10
- package/dist/apps/www/_layout.d.ts +5 -1
- package/dist/apps/www/_layout.js +3 -2
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +30 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +41 -11
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +11 -9
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +30 -24
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +332 -48
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +10 -5
- package/dist/apps/www/settings/appearance/index.d.ts +4 -0
- package/dist/apps/www/settings/appearance/index.js +13 -0
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +6 -7
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +106 -96
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +6 -2
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +9 -3
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +8 -9
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +6 -7
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +22 -34
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +6 -2
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +9 -4
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +22 -16
- package/package.json +2 -2
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { HiOutlineExclamationTriangle, HiOutlineLockClosed } from "react-icons/hi2";
|
|
7
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
8
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
9
|
+
import { useResolvedTheme } from "../../../appearance/resolvedTheme.js";
|
|
10
|
+
import { formatColour } from "./color.js";
|
|
11
|
+
import { useThemeSurface } from "./themeSurface.js";
|
|
12
|
+
|
|
13
|
+
export interface EncryptedBodyProps {
|
|
14
|
+
/** This device has no unlocked key session for the message's mailbox: unlocking could help. */
|
|
15
|
+
locked: boolean;
|
|
16
|
+
/** Why the message couldn't be decrypted when the keys were available (shown for the failed state). */
|
|
17
|
+
reason?: string;
|
|
18
|
+
/** Opens the app's unlock prompt (`useUnlockPrompt().requestUnlock`). Only used - and only offered - when `locked`. */
|
|
19
|
+
onUnlock: () => void;
|
|
20
|
+
/** An unlock is being asked for: the button waits. */
|
|
21
|
+
unlocking?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 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
|
|
26
|
+
* it stays legible over a translucent card and a background photo.
|
|
27
|
+
*
|
|
28
|
+
* **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
|
|
29
|
+
* "Unlock to sign or encrypt" does. When it succeeds the pane decrypts and shows the message in place.
|
|
30
|
+
* **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,
|
|
31
|
+
* and no button - unlocking again wouldn't change it.
|
|
32
|
+
*/
|
|
33
|
+
export default function EncryptedBody({ locked, reason, onUnlock, unlocking = false }: EncryptedBodyProps) {
|
|
34
|
+
const theme = useResolvedTheme();
|
|
35
|
+
const surface = useThemeSurface(theme);
|
|
36
|
+
const Icon = locked ? HiOutlineLockClosed : HiOutlineExclamationTriangle;
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className="flex flex-col items-center gap-2 rounded-md border border-border px-4 py-8 text-center"
|
|
40
|
+
style={{ backgroundColor: formatColour(surface.background) }}
|
|
41
|
+
>
|
|
42
|
+
<span
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
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(" ")}
|
|
45
|
+
>
|
|
46
|
+
<Icon size={24} />
|
|
47
|
+
</span>
|
|
48
|
+
<p className="text-base font-semibold text-text">{locked ? "This message is encrypted" : "This message can’t be decrypted"}</p>
|
|
49
|
+
{locked ? (
|
|
50
|
+
<>
|
|
51
|
+
<p className="text-sm text-text-muted">Unlock your keys to read it</p>
|
|
52
|
+
<Button type="button" className="!w-auto mt-1" aria-label="Unlock to view this message" loading={unlocking} disabled={unlocking} onClick={onUnlock}>
|
|
53
|
+
Unlock
|
|
54
|
+
</Button>
|
|
55
|
+
</>
|
|
56
|
+
) : (
|
|
57
|
+
reason && (
|
|
58
|
+
<div className="mt-1 max-w-prose text-left">
|
|
59
|
+
<Alert>{reason}</Alert>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { HiOutlineLockClosed } from "react-icons/hi2";
|
|
7
|
+
|
|
8
|
+
/** The subject an encrypted message carries on the server (RFC 9788's placeholder): the real one is inside the ciphertext. */
|
|
9
|
+
export const ENCRYPTED_SUBJECT_PLACEHOLDER = "[...]";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 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
|
|
13
|
+
* derives a preview for encrypted mail (it has no plaintext), so the line would otherwise be empty.
|
|
14
|
+
*/
|
|
15
|
+
export function EncryptedPreview() {
|
|
16
|
+
return (
|
|
17
|
+
<span className="inline-flex items-center gap-1">
|
|
18
|
+
<HiOutlineLockClosed size={12} aria-hidden="true" className="shrink-0" />
|
|
19
|
+
Encrypted message
|
|
20
|
+
</span>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 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
|
|
26
|
+
* and the placeholder subject - which a plaintext message with an empty body does not.
|
|
27
|
+
*/
|
|
28
|
+
export function conversationLooksEncrypted(conversation: { subject: string; latestPreview: string }): boolean {
|
|
29
|
+
return conversation.latestPreview === "" && conversation.subject === ENCRYPTED_SUBJECT_PLACEHOLDER;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 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. */
|
|
33
|
+
export function displaySubject(subject: string): string {
|
|
34
|
+
return subject === ENCRYPTED_SUBJECT_PLACEHOLDER ? "Encrypted message" : subject;
|
|
35
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { ApiRequestError, apiUrl } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import type { Attachment } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
|
+
import type { MimeAttachment } from "@rapidmx/react-shared/crypto/mime.js";
|
|
9
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
|
+
import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
|
+
import { useResolvedTheme } from "../../../appearance/resolvedTheme.js";
|
|
13
|
+
import { BodyContent, attachmentSource, cachedBodyContent, fetchBodyContent, makeCidResolver } from "./bodyContent.js";
|
|
14
|
+
import { prepareBodyHtml } from "./bodyHtml.js";
|
|
15
|
+
import { formatColour } from "./color.js";
|
|
16
|
+
import { controlFrame } from "./frameControl.js";
|
|
17
|
+
import { FRAME_SANDBOX, FrameMode, buildFrameDocument } from "./frameDocument.js";
|
|
18
|
+
import { surfaceKey, useThemeSurface } from "./themeSurface.js";
|
|
19
|
+
|
|
20
|
+
export interface MessageBodyProps {
|
|
21
|
+
messageUid: string;
|
|
22
|
+
/** Part of what a remembered body is keyed on, so an edited message is fetched again. */
|
|
23
|
+
messageVersion: number;
|
|
24
|
+
/** The accessible name of the body. */
|
|
25
|
+
title: string;
|
|
26
|
+
/** A body the caller already has - decrypted or verified content. Shown instead of asking the server. */
|
|
27
|
+
content?: BodyContent;
|
|
28
|
+
/** The message's server-side attachments, for resolving its inline `cid:` images. */
|
|
29
|
+
attachments?: Attachment[];
|
|
30
|
+
/** The parts inside a signed or encrypted message, for resolving its inline images from the decrypted content. */
|
|
31
|
+
inlineParts?: MimeAttachment[];
|
|
32
|
+
/** Show the message as its author wrote it: nothing adapted to the theme. */
|
|
33
|
+
original?: boolean;
|
|
34
|
+
/** Says whether "view original" would change anything, once that is known: never for plain text, a message with its own dark styles, or a
|
|
35
|
+
* message the theme leaves as it is. */
|
|
36
|
+
onAdaptable?: (adaptable: boolean) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Remote = { status: "loading" } | { status: "ready"; content: BodyContent } | { status: "error"; message: string };
|
|
40
|
+
|
|
41
|
+
/** Placeholder lines shown where the body will be, in the body's own place, until it has arrived and been laid out. */
|
|
42
|
+
export function BodySkeleton() {
|
|
43
|
+
return (
|
|
44
|
+
<div role="status" aria-busy="true" className="flex flex-col gap-2.5 py-1">
|
|
45
|
+
<span className="sr-only">Loading the message</span>
|
|
46
|
+
<Skeleton width="w-11/12" />
|
|
47
|
+
<Skeleton width="w-4/5" />
|
|
48
|
+
<Skeleton width="w-9/12" />
|
|
49
|
+
<Skeleton width="w-1/2" />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A short, stable fingerprint of a document's text (djb2), to tell one loaded document from the next. */
|
|
55
|
+
function fingerprint(text: string): string {
|
|
56
|
+
let hash = 5381;
|
|
57
|
+
for (let i = 0; i < text.length; i++) {
|
|
58
|
+
hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0;
|
|
59
|
+
}
|
|
60
|
+
return (hash >>> 0).toString(36);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The background of a message shown as authored: white, the canvas an HTML message is written for. */
|
|
64
|
+
const AUTHORED_CANVAS = "rgb(255, 255, 255)";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 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
|
|
68
|
+
* images load and the width changes), or plain text. See `frameDocument.ts` for the isolation and `themeAdaptation.ts` for how a message
|
|
69
|
+
* takes the app's theme.
|
|
70
|
+
*
|
|
71
|
+
* 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.
|
|
72
|
+
*/
|
|
73
|
+
export default function MessageBody({ messageUid, messageVersion, title, content, attachments, inlineParts, original = false, onAdaptable }: MessageBodyProps) {
|
|
74
|
+
const [remote, setRemote] = useState<Remote>(() => {
|
|
75
|
+
const cached = cachedBodyContent(messageUid, messageVersion);
|
|
76
|
+
return cached ? { status: "ready", content: cached } : { status: "loading" };
|
|
77
|
+
});
|
|
78
|
+
const [attempt, setAttempt] = useState(0);
|
|
79
|
+
const supplied = content !== undefined;
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (supplied) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const cached = cachedBodyContent(messageUid, messageVersion);
|
|
86
|
+
if (cached) {
|
|
87
|
+
setRemote({ status: "ready", content: cached });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
setRemote({ status: "loading" });
|
|
92
|
+
fetchBodyContent(messageUid, messageVersion, controller.signal).then(
|
|
93
|
+
(loaded) => setRemote({ status: "ready", content: loaded }),
|
|
94
|
+
(err) => {
|
|
95
|
+
if (!controller.signal.aborted) {
|
|
96
|
+
setRemote({ status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message." });
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
return () => controller.abort();
|
|
101
|
+
}, [messageUid, messageVersion, supplied, attempt]);
|
|
102
|
+
|
|
103
|
+
const shown: Remote = content ? { status: "ready", content } : remote;
|
|
104
|
+
if (shown.status === "loading") {
|
|
105
|
+
return <BodySkeleton />;
|
|
106
|
+
}
|
|
107
|
+
if (shown.status === "error") {
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex flex-col items-start gap-2">
|
|
110
|
+
<Alert>{shown.message}</Alert>
|
|
111
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setAttempt((n) => n + 1)}>
|
|
112
|
+
Try again
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (shown.content.kind === "text") {
|
|
118
|
+
// Rendered as text - React escapes it - never as markup, in the theme's own colours.
|
|
119
|
+
return <pre aria-label={title} className="m-0 text-sm font-sans whitespace-pre-wrap break-words text-text">{shown.content.text}</pre>;
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
<HtmlBody
|
|
123
|
+
key={messageUid}
|
|
124
|
+
messageUid={messageUid}
|
|
125
|
+
title={title}
|
|
126
|
+
html={shown.content.html}
|
|
127
|
+
remoteAvailable={!supplied}
|
|
128
|
+
attachments={attachments}
|
|
129
|
+
inlineParts={inlineParts}
|
|
130
|
+
original={original}
|
|
131
|
+
onAdaptable={onAdaptable}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function HtmlBody({
|
|
137
|
+
messageUid,
|
|
138
|
+
title,
|
|
139
|
+
html,
|
|
140
|
+
remoteAvailable,
|
|
141
|
+
attachments,
|
|
142
|
+
inlineParts,
|
|
143
|
+
original,
|
|
144
|
+
onAdaptable,
|
|
145
|
+
}: {
|
|
146
|
+
messageUid: string;
|
|
147
|
+
title: string;
|
|
148
|
+
html: string;
|
|
149
|
+
/** The full message can be opened on its own from the server (it was not supplied by the caller). */
|
|
150
|
+
remoteAvailable: boolean;
|
|
151
|
+
attachments?: Attachment[];
|
|
152
|
+
inlineParts?: MimeAttachment[];
|
|
153
|
+
original: boolean;
|
|
154
|
+
onAdaptable?: (adaptable: boolean) => void;
|
|
155
|
+
}) {
|
|
156
|
+
const theme = useResolvedTheme();
|
|
157
|
+
const surface = useThemeSurface(theme);
|
|
158
|
+
// Keyed on what the resolver reads rather than on the arrays, which a caller may rebuild on every render.
|
|
159
|
+
const inlineKey = [...(attachments ?? []).map((a) => `${a.uid}:${(a as { contentId?: string }).contentId ?? ""}`), inlineParts ? `parts:${inlineParts.length}` : ""].join("|");
|
|
160
|
+
const resolveCid = useMemo(() => makeCidResolver(attachments, inlineParts), [inlineKey]);
|
|
161
|
+
const prepared = useMemo(() => prepareBodyHtml(html, { resolveCid }), [html, resolveCid]);
|
|
162
|
+
const mode: FrameMode = prepared.declaresDarkSupport ? "native" : original ? "original" : "adapt";
|
|
163
|
+
const surfaceCss = formatColour(surface.background);
|
|
164
|
+
const srcDoc = useMemo(() => {
|
|
165
|
+
// The frame may load images from this server's attachment URLs only when an inline image was actually resolved to one.
|
|
166
|
+
const source = prepared.inlineImages > 0 ? attachmentSource() : undefined;
|
|
167
|
+
return buildFrameDocument(prepared.html, {
|
|
168
|
+
mode,
|
|
169
|
+
scheme: theme,
|
|
170
|
+
surface: surfaceCss,
|
|
171
|
+
text: formatColour(surface.text),
|
|
172
|
+
attachmentSource: source && prepared.html.includes(source) ? source : undefined,
|
|
173
|
+
});
|
|
174
|
+
}, [prepared, mode, theme, surfaceCss, surface.text]);
|
|
175
|
+
// 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
|
|
176
|
+
// one, and the old one's readiness is not the new one's.
|
|
177
|
+
const frameKey = useMemo(() => `${mode}:${surfaceKey(surface)}:${fingerprint(srcDoc)}`, [mode, surface, srcDoc]);
|
|
178
|
+
|
|
179
|
+
const [height, setHeight] = useState<number | undefined>(undefined);
|
|
180
|
+
const [readyFor, setReadyFor] = useState<string | null>(null);
|
|
181
|
+
const [canvasAuthored, setCanvasAuthored] = useState(false);
|
|
182
|
+
const dispose = useRef<(() => void) | undefined>(undefined);
|
|
183
|
+
useEffect(() => () => dispose.current?.(), []);
|
|
184
|
+
const reportAdaptable = useRef(onAdaptable);
|
|
185
|
+
reportAdaptable.current = onAdaptable;
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (mode === "native") {
|
|
188
|
+
reportAdaptable.current?.(false);
|
|
189
|
+
}
|
|
190
|
+
}, [mode]);
|
|
191
|
+
|
|
192
|
+
if (prepared.status === "too_large") {
|
|
193
|
+
return (
|
|
194
|
+
<Alert>
|
|
195
|
+
This message is too large to show here.{" "}
|
|
196
|
+
{remoteAvailable && (
|
|
197
|
+
<a href={apiUrl(`/mail/messages/${encodeURIComponent(messageUid)}/content`)} target="_blank" rel="noopener noreferrer" className="font-medium underline">
|
|
198
|
+
Open it in its own tab
|
|
199
|
+
</a>
|
|
200
|
+
)}
|
|
201
|
+
</Alert>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const ready = readyFor === frameKey;
|
|
206
|
+
const canvas = mode === "original" ? AUTHORED_CANVAS : surfaceCss;
|
|
207
|
+
const framed = mode !== "adapt" || canvasAuthored;
|
|
208
|
+
return (
|
|
209
|
+
<div className="relative rounded-md overflow-hidden" style={{ backgroundColor: canvas }}>
|
|
210
|
+
{!ready && <BodySkeleton />}
|
|
211
|
+
<iframe
|
|
212
|
+
key={frameKey}
|
|
213
|
+
title={title}
|
|
214
|
+
srcDoc={srcDoc}
|
|
215
|
+
sandbox={FRAME_SANDBOX}
|
|
216
|
+
scrolling="no"
|
|
217
|
+
onLoad={(event) => {
|
|
218
|
+
dispose.current?.();
|
|
219
|
+
dispose.current = controlFrame(event.currentTarget, {
|
|
220
|
+
surface: mode === "adapt" ? surface : undefined,
|
|
221
|
+
onHeight: setHeight,
|
|
222
|
+
onReady: (adaptation) => {
|
|
223
|
+
setReadyFor(frameKey);
|
|
224
|
+
if (adaptation) {
|
|
225
|
+
setCanvasAuthored(adaptation.authoredCanvas);
|
|
226
|
+
reportAdaptable.current?.(adaptation.material);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}}
|
|
231
|
+
className={[
|
|
232
|
+
"block w-full border-0 rounded-md",
|
|
233
|
+
ready ? "" : "absolute inset-x-0 top-0 invisible",
|
|
234
|
+
framed && ready ? "ring-1 ring-border" : "",
|
|
235
|
+
].join(" ")}
|
|
236
|
+
style={{ height: height ?? 0 }}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import ContactAvatar from "@rapidmx/react-shared/components/avatar/ContactAvatar.js";
|
|
7
|
+
import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
8
|
+
import type { MailAddressLike } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
9
|
+
import MailAddress from "../MailAddress.js";
|
|
10
|
+
import { UnreadBar, UnreadLabel } from "../unreadStyle.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The pieces the reading pane is drawn from: a header card that carries the subject, and one card per message. Built from the app's own
|
|
14
|
+
* tokens only (`bg-surface`, `text-text`, `border-border`), so they follow the theme, a branding palette and the Appearance colours; the
|
|
15
|
+
* body area inside a card paints its own opaque surface (`MessageBody`), so a translucent card over a background photo stays legible.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The header-style card at the top of the pane: the conversation's (or message's) subject, and what goes with it. */
|
|
19
|
+
export function SubjectCard({
|
|
20
|
+
subject,
|
|
21
|
+
meta,
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
subject: string;
|
|
25
|
+
/** A line under the subject, such as "3 messages". */
|
|
26
|
+
meta?: React.ReactNode;
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<header className="shrink-0 mx-2 mt-2 sm:mx-3 sm:mt-3 rounded-lg border border-border bg-surface backdrop-blur-sm shadow-sm px-4 py-2.5">
|
|
31
|
+
<h1 className="text-base sm:text-lg font-bold tracking-tight line-clamp-2 break-words">{subject}</h1>
|
|
32
|
+
{meta && <p className="text-xs sm:text-sm text-text-muted mt-0.5">{meta}</p>}
|
|
33
|
+
{children}
|
|
34
|
+
</header>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One message's card: rounded, bordered, lifted a little, its content clipped to the corners. The accent bar of an unread message runs down its left edge. */
|
|
39
|
+
export function CardShell({
|
|
40
|
+
unread = false,
|
|
41
|
+
className = "",
|
|
42
|
+
children,
|
|
43
|
+
}: {
|
|
44
|
+
unread?: boolean;
|
|
45
|
+
className?: string;
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={["relative rounded-lg border border-border bg-surface backdrop-blur-sm shadow-sm overflow-hidden", className].join(" ")}>
|
|
50
|
+
<UnreadBar unread={unread} />
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Headings carry the display typeface globally (`app.css`, unlayered - so no utility class can beat it); a sender line that happens to be a
|
|
58
|
+
* heading is body text and keeps the body's, by inline style.
|
|
59
|
+
*/
|
|
60
|
+
export const BODY_FONT_STYLE: React.CSSProperties = { fontFamily: "var(--rr-font-family)" };
|
|
61
|
+
|
|
62
|
+
/** A sender's avatar in the card header. There are no photos in the data, so this is the initials disc every contact gets. */
|
|
63
|
+
export function SenderAvatar({ from }: { from: MailAddressLike }) {
|
|
64
|
+
return <ContactAvatar displayName={from.displayName || from.name || from.address} size={40} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A collapsed message: its card with the sender, the date and the first line of the body, all of it the one button that expands it.
|
|
69
|
+
* Shares the header's look with an expanded card, so expanding does not change how the message begins.
|
|
70
|
+
*/
|
|
71
|
+
export function CollapsedCard({
|
|
72
|
+
from,
|
|
73
|
+
date,
|
|
74
|
+
preview,
|
|
75
|
+
unread,
|
|
76
|
+
buttonProps,
|
|
77
|
+
buttonRef,
|
|
78
|
+
senderClassName,
|
|
79
|
+
dateClassName,
|
|
80
|
+
}: {
|
|
81
|
+
from: MailAddressLike;
|
|
82
|
+
date: string;
|
|
83
|
+
preview: React.ReactNode;
|
|
84
|
+
unread: boolean;
|
|
85
|
+
buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
86
|
+
buttonRef: (node: HTMLButtonElement | null) => void;
|
|
87
|
+
senderClassName: string;
|
|
88
|
+
dateClassName: string;
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<CardShell unread={unread}>
|
|
92
|
+
<h2 style={BODY_FONT_STYLE}>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
ref={buttonRef}
|
|
96
|
+
{...buttonProps}
|
|
97
|
+
className={[
|
|
98
|
+
"w-full text-left flex items-center gap-3 px-4 py-3",
|
|
99
|
+
unread ? "bg-primary/[0.07] hover:bg-primary/10" : "hover:bg-surface-alt",
|
|
100
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary",
|
|
101
|
+
].join(" ")}
|
|
102
|
+
>
|
|
103
|
+
<UnreadLabel unread={unread} />
|
|
104
|
+
<SenderAvatar from={from} />
|
|
105
|
+
<span className="flex-1 min-w-0">
|
|
106
|
+
{/* On a phone the date drops under the sender rather than squeezing the name to nothing. */}
|
|
107
|
+
<span className="flex flex-wrap items-baseline justify-between gap-x-2 text-sm">
|
|
108
|
+
<MailAddress recipient={from} className={`${senderClassName} min-w-[13rem]! flex-1`} />
|
|
109
|
+
<span className={["text-xs shrink-0", dateClassName].join(" ")}>{date}</span>
|
|
110
|
+
</span>
|
|
111
|
+
<span className="block text-xs text-text-muted truncate font-normal">{preview}</span>
|
|
112
|
+
</span>
|
|
113
|
+
</button>
|
|
114
|
+
</h2>
|
|
115
|
+
</CardShell>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The card's place-holder while its message is still loading: an avatar, two header lines and a few lines of body. */
|
|
120
|
+
export function CardSkeleton() {
|
|
121
|
+
return (
|
|
122
|
+
<div aria-hidden="true" className="rounded-lg border border-border bg-surface shadow-sm overflow-hidden">
|
|
123
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
124
|
+
<Skeleton width="w-10" height="h-10" className="rounded-full shrink-0" />
|
|
125
|
+
<div className="flex-1 flex flex-col gap-2">
|
|
126
|
+
<Skeleton width="w-1/3" />
|
|
127
|
+
<Skeleton width="w-1/2" height="h-3" />
|
|
128
|
+
</div>
|
|
129
|
+
<Skeleton width="w-16" height="h-3" />
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex flex-col gap-2.5 px-4 pb-4">
|
|
132
|
+
<Skeleton width="w-11/12" />
|
|
133
|
+
<Skeleton width="w-4/5" />
|
|
134
|
+
<Skeleton width="w-9/12" />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What the pane shows while it is getting ready - its code loading, a conversation's messages arriving: the subject card as soon as the
|
|
142
|
+
* subject is known (`subject` is `undefined` when it is not, as on the mobile page before its message arrived) and a skeleton card per
|
|
143
|
+
* message, so the frame of the pane is on screen at once and the messages only fill in. One live region for the whole thing.
|
|
144
|
+
*/
|
|
145
|
+
export function ReadingPaneSkeleton({ subject, messageCount = 1 }: { subject?: string; messageCount?: number }) {
|
|
146
|
+
return (
|
|
147
|
+
<div className="flex-1 min-w-0 min-h-0 flex flex-col">
|
|
148
|
+
{subject !== undefined ? (
|
|
149
|
+
<SubjectCard subject={subject || "(no subject)"} meta={messageCount > 1 ? `${messageCount} messages` : undefined} />
|
|
150
|
+
) : (
|
|
151
|
+
<div aria-hidden="true" className="shrink-0 mx-2 mt-2 sm:mx-3 sm:mt-3 rounded-lg border border-border bg-surface shadow-sm px-4 py-3">
|
|
152
|
+
<Skeleton width="w-2/3" height="h-6" />
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
<SkeletonCards messageCount={messageCount} />
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The skeleton cards themselves - up to three, one live region ("Loading the message") for all of them - in the place the message cards take,
|
|
162
|
+
* so a pane that already shows its subject card (a thread whose messages are still coming) swaps them for the real ones without the header
|
|
163
|
+
* card being drawn again.
|
|
164
|
+
*/
|
|
165
|
+
export function SkeletonCards({ messageCount = 1 }: { messageCount?: number }) {
|
|
166
|
+
return (
|
|
167
|
+
<div role="status" aria-busy="true" className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">
|
|
168
|
+
<span className="sr-only">Loading the message</span>
|
|
169
|
+
{Array.from({ length: Math.min(3, Math.max(1, messageCount)) }, (_, index) => (
|
|
170
|
+
<CardSkeleton key={index} />
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ApiRequestError, apiUrl } from "@rapidmx/react-shared/util/api.js";
|
|
6
|
+
import { Attachment, attachmentContentUrl } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
import type { MimeAttachment } from "@rapidmx/react-shared/crypto/mime.js";
|
|
8
|
+
|
|
9
|
+
/** What a message's body is: server-sanitized HTML, or plain text (a message with no HTML part, or the server's plain-text fallback). */
|
|
10
|
+
export type BodyContent = { kind: "html"; html: string } | { kind: "text"; text: string };
|
|
11
|
+
|
|
12
|
+
/** How many bodies are remembered for the session - a thread that is expanded, collapsed and expanded again does not ask the server twice. */
|
|
13
|
+
const CACHE_LIMIT = 40;
|
|
14
|
+
/** What the pane asks `/content` for. */
|
|
15
|
+
export const ACCEPT = "text/html, text/plain;q=0.8";
|
|
16
|
+
const cache = new Map<string, BodyContent>();
|
|
17
|
+
|
|
18
|
+
/** Forgets every remembered body (sign-out, and the tests). */
|
|
19
|
+
export function clearBodyContentCache(): void {
|
|
20
|
+
cache.clear();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The remembered body for a message at a version, if any. */
|
|
24
|
+
export function cachedBodyContent(uid: string, version: number): BodyContent | undefined {
|
|
25
|
+
return cache.get(`${uid}:${version}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The message's body from the server's `GET /mail/messages/:id/content`: the HTML the server sanitized when it ingested the message
|
|
30
|
+
* (`sanitizedHtmlBlobKey`), or - for a message with none - the plain-text preview. A request that fails throws an `ApiRequestError`.
|
|
31
|
+
* Successful answers are remembered per uid and version.
|
|
32
|
+
*/
|
|
33
|
+
export async function fetchBodyContent(uid: string, version: number, signal?: AbortSignal): Promise<BodyContent> {
|
|
34
|
+
const cached = cachedBodyContent(uid, version);
|
|
35
|
+
if (cached) {
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
// Says what it can show, as a document request would: the server answers with the sanitized HTML, or its plain-text fallback.
|
|
39
|
+
const res = await fetch(apiUrl(`/mail/messages/${encodeURIComponent(uid)}/content`), {
|
|
40
|
+
credentials: "include",
|
|
41
|
+
headers: { Accept: ACCEPT },
|
|
42
|
+
signal,
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
46
|
+
const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
|
|
47
|
+
throw new ApiRequestError((body && (body.message || body.error)) || res.statusText || "Could not load this message.", res.status, body?.code, body);
|
|
48
|
+
}
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
const content: BodyContent = (res.headers.get("content-type") ?? "").toLowerCase().includes("text/html")
|
|
51
|
+
? { kind: "html", html: text }
|
|
52
|
+
: { kind: "text", text };
|
|
53
|
+
if (cache.size >= CACHE_LIMIT) {
|
|
54
|
+
cache.delete(cache.keys().next().value as string);
|
|
55
|
+
}
|
|
56
|
+
cache.set(`${uid}:${version}`, content);
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The largest inline image embedded into a body from a signed or encrypted message. */
|
|
61
|
+
export const MAX_INLINE_IMAGE_BYTES = 5_000_000;
|
|
62
|
+
const INLINE_IMAGE_TYPES = /^image\/(?:png|jpe?g|gif|webp|avif|bmp|svg\+xml)$/;
|
|
63
|
+
|
|
64
|
+
function base64(bytes: Uint8Array): string {
|
|
65
|
+
let binary = "";
|
|
66
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
67
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
68
|
+
}
|
|
69
|
+
return btoa(binary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A server attachment record as it may really arrive: the model carries the part's `Content-ID`, though the client type doesn't list it. */
|
|
73
|
+
export type InlineCapable = Attachment & { contentId?: string };
|
|
74
|
+
|
|
75
|
+
/** The absolute URL of an attachment - the frame is a document of its own, where a relative URL would not mean what it does here. */
|
|
76
|
+
export function absoluteAttachmentUrl(uid: string): string {
|
|
77
|
+
return new URL(attachmentContentUrl(uid), window.location.href).href;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The CSP source that allows exactly this server's attachment URLs and nothing else. */
|
|
81
|
+
export function attachmentSource(): string {
|
|
82
|
+
return new URL(apiUrl("/mail/attachments/"), window.location.href).href;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* How an inline `cid:` image is resolved to something the frame may load: for a message shown from the server, the attachment whose
|
|
87
|
+
* `Content-ID` matches (its URL on this server); for a decrypted or verified one, the part inside the signed/encrypted entity, embedded
|
|
88
|
+
* as a `data:` URI (raster and SVG images up to `MAX_INLINE_IMAGE_BYTES`). Anything else has no source, so the image is not shown.
|
|
89
|
+
*/
|
|
90
|
+
export function makeCidResolver(attachments: InlineCapable[] | undefined, parts: MimeAttachment[] | undefined): (contentId: string) => string | undefined {
|
|
91
|
+
return (contentId) => {
|
|
92
|
+
if (parts) {
|
|
93
|
+
const part = parts.find((candidate) => candidate.contentId === contentId);
|
|
94
|
+
const bytes = part && INLINE_IMAGE_TYPES.test(part.contentType) ? part.decode() : undefined;
|
|
95
|
+
return part && bytes && bytes.length <= MAX_INLINE_IMAGE_BYTES ? `data:${part.contentType};base64,${base64(bytes)}` : undefined;
|
|
96
|
+
}
|
|
97
|
+
const attachment = attachments?.find((candidate) => candidate.contentId === contentId);
|
|
98
|
+
return attachment ? absoluteAttachmentUrl(attachment.uid) : undefined;
|
|
99
|
+
};
|
|
100
|
+
}
|