@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
package/dist/apps/www/index.js
CHANGED
|
@@ -3,33 +3,44 @@ 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 {
|
|
6
|
+
import { routedPage } from "./_routedPage.js";
|
|
7
|
+
import { useNavigate } from "../shared/navigation/AppRouter.js";
|
|
8
|
+
import { whenIdle } from "../shared/navigation/idle.js";
|
|
9
|
+
import { prefetchComposeWindow } from "../shared/components/mail/compose/ComposeContext.js";
|
|
10
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
11
|
import { HiOutlineFlag, HiOutlineLockClosed, HiOutlinePaperClip } from "react-icons/hi2";
|
|
8
12
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
-
import { archiveMessage, bulkUpdateMessages, createFolder, getMessage, getMessageRawContent, listMessages, moveMessages, setMessagesFlagged,
|
|
13
|
+
import { archiveMessage, bulkUpdateMessages, createFolder, getMessage, getMessageRawContent, listFolders, listMessages, moveMessages, setMessagesFlagged, } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
14
|
import { listLabels } from "@rapidmx/react-shared/mail/labelsApi.js";
|
|
11
15
|
import { listConversationMessages, listConversations, } from "@rapidmx/react-shared/mail/conversationsApi.js";
|
|
12
16
|
import { search as searchMailbox } from "@rapidmx/react-shared/search/searchApi.js";
|
|
13
17
|
import { parseSearchQuery } from "@rapidmx/react-shared/search/queryGrammar.js";
|
|
14
18
|
import { normalizeServerScores } from "@rapidmx/react-shared/search/searchScoring.js";
|
|
15
|
-
import { searchEncryptedCandidates } from "@rapidmx/react-shared/search/searchTier3.js";
|
|
16
19
|
import { searchLocalIndex } from "../shared/search/searchTier2.js";
|
|
17
20
|
import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
18
|
-
import {
|
|
19
|
-
import { useMarkMessageRead, useMessageAttachments } from "@rapidmx/react-shared/mail/mailDetailHooks.js";
|
|
21
|
+
import { useMessageAttachments } from "@rapidmx/react-shared/mail/mailDetailHooks.js";
|
|
20
22
|
import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
|
|
21
23
|
import MailShell, { useMailShell, } from "../shared/components/mail/layout/MailShell.js";
|
|
22
24
|
import MailAddress from "../shared/components/mail/MailAddress.js";
|
|
25
|
+
import OutboxRowStatus from "../shared/components/mail/OutboxRowStatus.js";
|
|
23
26
|
import { mergeFirstPage } from "../shared/mail/mergeFirstPage.js";
|
|
24
|
-
import
|
|
27
|
+
import { listSnapshotKey, readListSnapshot, saveListScroll, writeListSnapshot } from "../shared/mail/listSnapshots.js";
|
|
28
|
+
import { setReadStateMany } from "../shared/mail/messageReadState.js";
|
|
29
|
+
import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
|
|
30
|
+
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
|
|
31
|
+
import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
|
|
25
32
|
import ConversationList from "../shared/components/mail/ConversationList.js";
|
|
26
|
-
import
|
|
33
|
+
import { EncryptedPreview } from "../shared/components/mail/reading/EncryptedPreview.js";
|
|
27
34
|
import MailListToolbar from "../shared/components/mail/MailListToolbar.js";
|
|
28
35
|
import MailSelectionBar from "../shared/components/mail/MailSelectionBar.js";
|
|
29
36
|
import { CONVERSATION_SORT_NOTE, CONVERSATION_SORT_UNAVAILABLE, MAIL_LIST_CLASSIFICATION_FILTERS, getMailListPreferences, setMailListPreferences, sortConversations, } from "../shared/components/mail/listPreferences.js";
|
|
30
37
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
31
|
-
import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
38
|
+
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
32
39
|
import { useUnlockPrompt } from "../shared/components/layout/UnlockPromptProvider.js";
|
|
40
|
+
import { SHORTCUTS } from "../shared/keyboard/keymap.js";
|
|
41
|
+
import { isActivatable } from "../shared/keyboard/targets.js";
|
|
42
|
+
import { useShortcut } from "../shared/keyboard/useShortcut.js";
|
|
43
|
+
import { notifyApiError } from "../shared/notifications/apiErrors.js";
|
|
33
44
|
const MESSAGE_PAGE_SIZE = 50;
|
|
34
45
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
35
46
|
const LIST_PREVIEW_MAX_LENGTH = 160;
|
|
@@ -70,6 +81,9 @@ async function decryptEncryptedRows(messages, unlocked) {
|
|
|
70
81
|
const entries = await Promise.all(encrypted.map(async (message) => {
|
|
71
82
|
try {
|
|
72
83
|
const rawMime = await getMessageRawContent(message.uid);
|
|
84
|
+
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
85
|
+
// no encrypted mail in it never needs it.
|
|
86
|
+
const { evaluateMessageSecurity } = await import("@rapidmx/react-shared/crypto/messageSecurity.js");
|
|
73
87
|
const security = await evaluateMessageSecurity(rawMime, unlocked);
|
|
74
88
|
if (!security.subject && !security.html) {
|
|
75
89
|
return null;
|
|
@@ -222,6 +236,8 @@ function tier3Windows(parsed, coverage, searchAllMail) {
|
|
|
222
236
|
/** Runs Tier 3 over each window and merges the candidates (a uid can't match in two disjoint windows, but a
|
|
223
237
|
* message whose date sits exactly on a boundary may come back from both). */
|
|
224
238
|
async function searchTier3Windows(windows, unlocked, mailboxUid) {
|
|
239
|
+
// Loaded on first use, with the S/MIME code it decrypts through (see `decryptEncryptedRows()`).
|
|
240
|
+
const { searchEncryptedCandidates } = await import("@rapidmx/react-shared/search/searchTier3.js");
|
|
225
241
|
const pages = await Promise.all(windows.map((window) => searchEncryptedCandidates(window, unlocked, TIER3_CANDIDATE_LIMIT, { mailboxUid })));
|
|
226
242
|
const merged = new Map();
|
|
227
243
|
for (const result of pages.flat()) {
|
|
@@ -334,16 +350,28 @@ async function fetchAggregateMessages(mailboxFolders, type, filter) {
|
|
|
334
350
|
}));
|
|
335
351
|
return mergeInboxMessages(perMailbox);
|
|
336
352
|
}
|
|
337
|
-
|
|
353
|
+
function InboxPage(props) {
|
|
338
354
|
return (_jsx(MailShell, { ...props, children: _jsx(InboxContent, { userUid: props.userUid }) }));
|
|
339
355
|
}
|
|
340
356
|
function InboxContent({ userUid }) {
|
|
341
|
-
const { folderUid, mailboxUid, mailboxes, mailboxFolders, aggregateFolderType, onFolderCreated, live } = useMailShell();
|
|
357
|
+
const { folderUid, mailboxUid, mailboxes, mailboxFolders, aggregateFolderType, onFolderCreated, noteFolderUids, live, trackMessageChange } = useMailShell();
|
|
342
358
|
const isMobile = useIsMobile();
|
|
359
|
+
const navigate = useNavigate();
|
|
343
360
|
const { requestUnlock } = useUnlockPrompt();
|
|
344
361
|
const [messages, setMessages] = useState([]);
|
|
345
362
|
const [conversations, setConversations] = useState([]);
|
|
346
363
|
const [loading, setLoading] = useState(true);
|
|
364
|
+
// Once a list is on screen and the browser has nothing better to do, fetch the code a click on a message (the reading pane)
|
|
365
|
+
// or on Compose/Reply (the compose window, with its editor) would otherwise wait for.
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (loading) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
return whenIdle(() => {
|
|
371
|
+
prefetchReadingPane();
|
|
372
|
+
prefetchComposeWindow();
|
|
373
|
+
});
|
|
374
|
+
}, [loading]);
|
|
347
375
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
348
376
|
const [hasMore, setHasMore] = useState(false);
|
|
349
377
|
const [error, setError] = useState(null);
|
|
@@ -368,7 +396,6 @@ function InboxContent({ userUid }) {
|
|
|
368
396
|
// or it would act on a selection that is still arriving.
|
|
369
397
|
const [resolvingSelection, setResolvingSelection] = useState(0);
|
|
370
398
|
const [bulkBusy, setBulkBusy] = useState(false);
|
|
371
|
-
const [bulkError, setBulkError] = useState(null);
|
|
372
399
|
// Bumped to force the list effect below to re-run - a bulk update is deliberately neither atomic nor
|
|
373
400
|
// all-or-nothing (see `bulkUpdateMessages()`), so a rejection means refetching rather than guessing
|
|
374
401
|
// which half of the selection actually landed.
|
|
@@ -481,6 +508,10 @@ function InboxContent({ userUid }) {
|
|
|
481
508
|
// removal steps this back too, or the next page would silently skip a message. See `loadMore()`.
|
|
482
509
|
const listedOffsetRef = useRef(0);
|
|
483
510
|
const scrollContainerRef = useRef(null);
|
|
511
|
+
const searchInputRef = useRef(null);
|
|
512
|
+
// Where the message the keyboard just removed from the list (deleted, archived, moved) was: the next Down/Up continues from that spot, so
|
|
513
|
+
// clearing an inbox from the keyboard walks down it instead of jumping back to the top. Cleared by any selection.
|
|
514
|
+
const removedAnchorRef = useRef(null);
|
|
484
515
|
// State (via a callback ref), not a plain ref: the sentinel mounts and unmounts as the list loads,
|
|
485
516
|
// filters, and empties, and the observer effect below must re-attach to whichever node is current.
|
|
486
517
|
const [sentinel, setSentinel] = useState(null);
|
|
@@ -652,6 +683,48 @@ function InboxContent({ userUid }) {
|
|
|
652
683
|
const handle = setTimeout(() => setSearchQuery(searchInput.trim()), SEARCH_DEBOUNCE_MS);
|
|
653
684
|
return () => clearTimeout(handle);
|
|
654
685
|
}, [searchInput]);
|
|
686
|
+
// Choosing another folder no longer loads a new page, so what was being searched for would follow the reader into it: the
|
|
687
|
+
// search box and its results are cleared instead, as the page load used to. (The first selection resolves after mount - the
|
|
688
|
+
// shell reads the URL and lists the folders in effects - and is not a change of folder.)
|
|
689
|
+
const selectionKey = `${mailboxUid ?? ""}|${folderUid ?? ""}|${aggregateFolderType ?? ""}`;
|
|
690
|
+
const shownSelectionRef = useRef(null);
|
|
691
|
+
useEffect(() => {
|
|
692
|
+
const shown = shownSelectionRef.current;
|
|
693
|
+
if (shown !== null && shown !== selectionKey) {
|
|
694
|
+
setSearchInput("");
|
|
695
|
+
setSearchQuery("");
|
|
696
|
+
}
|
|
697
|
+
if (folderUid || aggregateFolderType) {
|
|
698
|
+
shownSelectionRef.current = selectionKey;
|
|
699
|
+
}
|
|
700
|
+
}, [selectionKey]);
|
|
701
|
+
// Which listing this is, for the short-lived snapshot of each folder that makes going back to one instant (see
|
|
702
|
+
// `listSnapshots.ts`). A search, an aggregate view and a folder that hasn't resolved have no snapshot.
|
|
703
|
+
const listKey = listSnapshotKey({
|
|
704
|
+
mailboxUid: activeMailboxUid,
|
|
705
|
+
folderUid,
|
|
706
|
+
conversations: preferences.showAsConversations,
|
|
707
|
+
filter: effectiveFilter,
|
|
708
|
+
labels: labelFilterKey,
|
|
709
|
+
sort: serverSortKey,
|
|
710
|
+
});
|
|
711
|
+
const snapshotable = !!folderUid && !isSearching && !aggregateFolderType;
|
|
712
|
+
/** The key of the listing `messages`/`conversations` currently hold - not `listKey` while a new folder's rows are still coming. */
|
|
713
|
+
const shownListKeyRef = useRef(null);
|
|
714
|
+
/** A scroll position to put the list back at once its rows are on screen (a snapshot's). */
|
|
715
|
+
const pendingScrollRef = useRef(null);
|
|
716
|
+
useLayoutEffect(() => {
|
|
717
|
+
if (pendingScrollRef.current !== null && !loading && scrollContainerRef.current) {
|
|
718
|
+
scrollContainerRef.current.scrollTop = pendingScrollRef.current;
|
|
719
|
+
pendingScrollRef.current = null;
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
// Keeps the snapshot of the folder on screen current - rows added by "load more" or live updates, a read message, the selection.
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
if (snapshotable && !loading && shownListKeyRef.current === listKey) {
|
|
725
|
+
writeListSnapshot(listKey, { messages, conversations, hasMore, selectedUid });
|
|
726
|
+
}
|
|
727
|
+
}, [snapshotable, loading, listKey, messages, conversations, hasMore, selectedUid]);
|
|
655
728
|
// Loads whichever list the current folder, arrangement and search state call for, and resets every
|
|
656
729
|
// piece of per-listing state (selection, paging, select mode) that a previous listing left behind.
|
|
657
730
|
useEffect(() => {
|
|
@@ -678,6 +751,8 @@ function InboxContent({ userUid }) {
|
|
|
678
751
|
const invalidate = () => {
|
|
679
752
|
searchRunIdRef.current += 1;
|
|
680
753
|
};
|
|
754
|
+
// A folder shown a moment ago is put on screen at once and revalidated behind the rows; one that was not shows a skeleton.
|
|
755
|
+
const snapshot = snapshotable ? readListSnapshot(listKey) : undefined;
|
|
681
756
|
if (!folderUid && !aggregateFolderType) {
|
|
682
757
|
// The shell hasn't resolved this mailbox's folders yet (the render below says so). Listing
|
|
683
758
|
// anything now means a request whose answer is thrown away the moment the Inbox arrives - and
|
|
@@ -702,11 +777,30 @@ function InboxContent({ userUid }) {
|
|
|
702
777
|
// (always set - `MailShell` only renders this component once at least one mailbox exists).
|
|
703
778
|
// They're scoped to the selected folder (`folderUid`, absent only in aggregate mode), so the
|
|
704
779
|
// conversation list matches the folder the sidebar has selected rather than the whole mailbox.
|
|
705
|
-
|
|
780
|
+
if (snapshot) {
|
|
781
|
+
setConversations(snapshot.conversations);
|
|
782
|
+
listedOffsetRef.current = snapshot.conversations.length;
|
|
783
|
+
setHasMore(snapshot.hasMore);
|
|
784
|
+
shownListKeyRef.current = listKey;
|
|
785
|
+
pendingScrollRef.current = snapshot.scrollTop;
|
|
786
|
+
setLoading(false);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
setLoading(true);
|
|
790
|
+
}
|
|
706
791
|
setError(null);
|
|
707
792
|
listConversations(activeMailboxUid, conversationParams(0))
|
|
708
793
|
.then((result) => {
|
|
709
794
|
if (isCurrentRun()) {
|
|
795
|
+
shownListKeyRef.current = listKey;
|
|
796
|
+
if (snapshot) {
|
|
797
|
+
// Folded into what is already shown, as a live refresh does, so the reader's place is kept.
|
|
798
|
+
const merged = mergeFirstPage(snapshot.conversations, result, conversationKey, MESSAGE_PAGE_SIZE);
|
|
799
|
+
setConversations(merged.rows);
|
|
800
|
+
listedOffsetRef.current = merged.complete ? result.length : snapshot.conversations.length + merged.added;
|
|
801
|
+
setHasMore(!merged.complete);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
710
804
|
setConversations(result);
|
|
711
805
|
listedOffsetRef.current = result.length;
|
|
712
806
|
setHasMore(result.length === MESSAGE_PAGE_SIZE);
|
|
@@ -741,7 +835,7 @@ function InboxContent({ userUid }) {
|
|
|
741
835
|
});
|
|
742
836
|
return invalidate;
|
|
743
837
|
}
|
|
744
|
-
setLoading(
|
|
838
|
+
setLoading(!snapshot);
|
|
745
839
|
setError(null);
|
|
746
840
|
if (isSearching) {
|
|
747
841
|
setCoverage(undefined);
|
|
@@ -829,9 +923,28 @@ function InboxContent({ userUid }) {
|
|
|
829
923
|
}
|
|
830
924
|
// Set: the guard at the top of this effect returned for a view with neither a folder nor an
|
|
831
925
|
// aggregate type, and the aggregate branch above returned for the one with an aggregate type.
|
|
926
|
+
if (snapshot) {
|
|
927
|
+
setMessages(snapshot.messages);
|
|
928
|
+
listedOffsetRef.current = snapshot.messages.length;
|
|
929
|
+
setHasMore(snapshot.hasMore);
|
|
930
|
+
// The selection the reader left the folder with, if that message is still there.
|
|
931
|
+
setSelectedUid(snapshot.messages.some((m) => m.uid === snapshot.selectedUid) ? snapshot.selectedUid : null);
|
|
932
|
+
shownListKeyRef.current = listKey;
|
|
933
|
+
pendingScrollRef.current = snapshot.scrollTop;
|
|
934
|
+
}
|
|
832
935
|
listMessages(folderUid, listParams)
|
|
833
936
|
.then((results) => {
|
|
834
937
|
if (isCurrentRun()) {
|
|
938
|
+
shownListKeyRef.current = listKey;
|
|
939
|
+
if (snapshot) {
|
|
940
|
+
// Folded into what is already shown, as a live refresh does: the reader's place, selection and paged-in
|
|
941
|
+
// rows stay, and a row they changed since (a higher version) is not put back.
|
|
942
|
+
const merged = mergeFirstPage(snapshot.messages, results, messageUid, MESSAGE_PAGE_SIZE, (current, next) => current.version >= next.version ? current : next);
|
|
943
|
+
setMessages(merged.rows);
|
|
944
|
+
listedOffsetRef.current = merged.complete ? results.length : snapshot.messages.length + merged.added;
|
|
945
|
+
setHasMore(!merged.complete);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
835
948
|
setMessages(results);
|
|
836
949
|
listedOffsetRef.current = results.length;
|
|
837
950
|
setHasMore(results.length === MESSAGE_PAGE_SIZE);
|
|
@@ -869,9 +982,16 @@ function InboxContent({ userUid }) {
|
|
|
869
982
|
unlockRefresh,
|
|
870
983
|
searchAllMail,
|
|
871
984
|
aggregateFolderType,
|
|
872
|
-
|
|
985
|
+
// Only a merged view is built from the folder tree; for a single folder a folder appearing in the sidebar (the Outbox, Sent Items) must not reload
|
|
986
|
+
// the list on screen and forget its selection.
|
|
987
|
+
aggregateFolderType ? mailboxFolders : null,
|
|
873
988
|
activeMailboxUid,
|
|
874
989
|
]);
|
|
990
|
+
// Which folders the messages on screen live in: one the sidebar does not know (the Outbox and Sent Items are made by the server on first use) is looked for -
|
|
991
|
+
// its mailbox's folders are listed again, once - so it appears without a reload. A conversation names every folder it has a message in.
|
|
992
|
+
useEffect(() => {
|
|
993
|
+
noteFolderUids([...messages.map((message) => message.folderUid), ...conversations.flatMap((conversation) => conversation.folderUids)]);
|
|
994
|
+
}, [messages, conversations, noteFolderUids]);
|
|
875
995
|
// New mail without a reload. `live` is bumped by a push event, a reconnect, the safety-net poll or the tab coming back (see
|
|
876
996
|
// `useMailLiveUpdates()`); this then quietly refetches the first page of whatever is listed and folds it in - unlike the
|
|
877
997
|
// effect above it resets nothing: not the selection, the open thread, select mode, the scroll position or the rows
|
|
@@ -920,7 +1040,9 @@ function InboxContent({ userUid }) {
|
|
|
920
1040
|
return;
|
|
921
1041
|
}
|
|
922
1042
|
// A row the reader has just changed (a higher version) is not put back by a fetch that began before.
|
|
923
|
-
|
|
1043
|
+
// Equal versions are the same server state, so the row on screen - which may carry an optimistic read/unread
|
|
1044
|
+
// flip the server has not answered yet - wins too.
|
|
1045
|
+
const merged = mergeFirstPage(messagesRef.current, fresh, messageUid, MESSAGE_PAGE_SIZE, (current, next) => current.version >= next.version ? current : next);
|
|
924
1046
|
setMessages(merged.rows);
|
|
925
1047
|
listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
|
|
926
1048
|
setHasMore(!merged.complete);
|
|
@@ -1104,22 +1226,27 @@ function InboxContent({ userUid }) {
|
|
|
1104
1226
|
*
|
|
1105
1227
|
* A conversation's *parent* row has no copy to replace: it is a summary of the whole thread, and its
|
|
1106
1228
|
* "2 unread" chip and bold styling come from a count the server worked out when the list was fetched.
|
|
1107
|
-
* So when
|
|
1108
|
-
* count
|
|
1109
|
-
* read, until the whole list was reloaded.
|
|
1229
|
+
* So when a message changes read state (`previous` and `updated` differ in it - the optimistic copy of a message just
|
|
1230
|
+
* read, or its revert), that count moves by one here - otherwise a conversation kept claiming unread mail the reader
|
|
1231
|
+
* had just read, until the whole list was reloaded.
|
|
1110
1232
|
*/
|
|
1111
1233
|
function patchListedMessage(updated, previous) {
|
|
1112
1234
|
setMessages((prev) => prev.map((m) => (m.uid === updated.uid ? updated : m)));
|
|
1113
1235
|
// `ConversationList` fetched its own copy of this message when the thread was expanded; hand it the
|
|
1114
1236
|
// newer one so the child row doesn't keep showing a stale read/flag state.
|
|
1115
1237
|
setConversationPatches((prev) => ({ ...prev, [updated.uid]: updated }));
|
|
1116
|
-
|
|
1238
|
+
const unreadChange = previous ? Number(isUnread(updated)) - Number(isUnread(previous)) : 0;
|
|
1239
|
+
if (unreadChange !== 0) {
|
|
1117
1240
|
setConversations((prev) => prev.map((conversation) => conversation.messageUids.includes(updated.uid)
|
|
1118
|
-
? { ...conversation, unreadCount: Math.max(0, conversation.unreadCount
|
|
1241
|
+
? { ...conversation, unreadCount: Math.max(0, conversation.unreadCount + unreadChange) }
|
|
1119
1242
|
: conversation));
|
|
1120
1243
|
}
|
|
1121
1244
|
}
|
|
1122
1245
|
function removeListedMessages(uids) {
|
|
1246
|
+
const selectedIndex = messagesRef.current.findIndex((m) => m.uid === selectedUid);
|
|
1247
|
+
if (selectedUid && uids.has(selectedUid) && selectedIndex !== -1) {
|
|
1248
|
+
removedAnchorRef.current = selectedIndex;
|
|
1249
|
+
}
|
|
1123
1250
|
setMessages((prev) => prev.filter((m) => !uids.has(m.uid)));
|
|
1124
1251
|
listedOffsetRef.current = Math.max(0, listedOffsetRef.current - uids.size);
|
|
1125
1252
|
setSelectedUid((prev) => (prev && uids.has(prev) ? null : prev));
|
|
@@ -1172,7 +1299,6 @@ function InboxContent({ userUid }) {
|
|
|
1172
1299
|
setSelectMode(false);
|
|
1173
1300
|
setSelectedUids(new Set());
|
|
1174
1301
|
setSelectedConversationIds(new Set());
|
|
1175
|
-
setBulkError(null);
|
|
1176
1302
|
}
|
|
1177
1303
|
function toggleSelected(uid) {
|
|
1178
1304
|
setSelectedUids((prev) => {
|
|
@@ -1188,7 +1314,7 @@ function InboxContent({ userUid }) {
|
|
|
1188
1314
|
}
|
|
1189
1315
|
/** Loads (once) the messages behind each of `ids`, so a ticked conversation resolves to the messages
|
|
1190
1316
|
* every bulk action below acts on. If any of them fails to load, none of that batch stays ticked and
|
|
1191
|
-
*
|
|
1317
|
+
* a pop-up says why - better than acting on the part of a selection that happened to arrive. */
|
|
1192
1318
|
async function resolveConversations(ids) {
|
|
1193
1319
|
const missing = ids.filter((id) => !conversationMessagesById[id]);
|
|
1194
1320
|
if (missing.length === 0) {
|
|
@@ -1200,7 +1326,7 @@ function InboxContent({ userUid }) {
|
|
|
1200
1326
|
setConversationMessagesById((prev) => ({ ...prev, ...Object.fromEntries(loaded) }));
|
|
1201
1327
|
}
|
|
1202
1328
|
catch (err) {
|
|
1203
|
-
|
|
1329
|
+
notifyApiError(err, "Couldn't load the messages in one of those conversations");
|
|
1204
1330
|
setSelectedConversationIds((prev) => {
|
|
1205
1331
|
const next = new Set(prev);
|
|
1206
1332
|
for (const id of missing) {
|
|
@@ -1241,14 +1367,27 @@ function InboxContent({ userUid }) {
|
|
|
1241
1367
|
*
|
|
1242
1368
|
* A bulk update is applied element by element server-side and stops at its first rejection, so a
|
|
1243
1369
|
* failure leaves an unknown prefix of the selection already changed (see `bulkUpdateMessages()`) -
|
|
1244
|
-
* which is why a failure reloads the list rather than trying to reconcile it, and says so
|
|
1370
|
+
* which is why a failure reloads the list rather than trying to reconcile it, and says so ("all of those messages" in the pop-up's
|
|
1371
|
+
* title: some may have changed).
|
|
1372
|
+
*
|
|
1373
|
+
* `chosen` is the selection bar's ticked messages unless the caller (a keyboard shortcut acting on the one open message or conversation)
|
|
1374
|
+
* names its own. Resolves whether it succeeded.
|
|
1245
1375
|
*/
|
|
1246
|
-
async function runBulkAction(action, removesRows) {
|
|
1247
|
-
const chosen = selectedMessages;
|
|
1376
|
+
async function runBulkAction(action, removesRows, chosen = selectedMessages) {
|
|
1248
1377
|
setBulkBusy(true);
|
|
1249
|
-
setBulkError(null);
|
|
1250
1378
|
try {
|
|
1251
1379
|
const updated = await action(chosen);
|
|
1380
|
+
if (removesRows) {
|
|
1381
|
+
// A move (Delete, Archive, Report junk, Move to): the source folders' badges go down, the target's up. Reading
|
|
1382
|
+
// and unreading are tracked by `setReadStateMany()` itself, as they happen.
|
|
1383
|
+
const movedByUid = new Map(updated.map((m) => [m.uid, m]));
|
|
1384
|
+
for (const before of chosen) {
|
|
1385
|
+
const after = movedByUid.get(before.uid);
|
|
1386
|
+
if (after) {
|
|
1387
|
+
trackMessageChange(before, after).settle();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1252
1391
|
if (preferences.showAsConversations) {
|
|
1253
1392
|
// A conversation row is a summary of its messages - its count, unread count, participants
|
|
1254
1393
|
// and preview all move when a bulk action changes or empties part of it - so the list is
|
|
@@ -1265,13 +1404,15 @@ function InboxContent({ userUid }) {
|
|
|
1265
1404
|
setMessages((prev) => prev.map((m) => byUid.get(m.uid) ?? m));
|
|
1266
1405
|
}
|
|
1267
1406
|
setSelectedUids(new Set());
|
|
1407
|
+
return true;
|
|
1268
1408
|
}
|
|
1269
1409
|
catch (err) {
|
|
1270
|
-
|
|
1410
|
+
notifyApiError(err, chosen.length === 1 ? "Couldn't update the message" : "Couldn't update all of those messages");
|
|
1271
1411
|
setSelectedUids(new Set());
|
|
1272
1412
|
setSelectedConversationIds(new Set());
|
|
1273
1413
|
setConversationMessagesById({});
|
|
1274
1414
|
setRefreshKey((n) => n + 1);
|
|
1415
|
+
return false;
|
|
1275
1416
|
}
|
|
1276
1417
|
finally {
|
|
1277
1418
|
setBulkBusy(false);
|
|
@@ -1290,12 +1431,13 @@ function InboxContent({ userUid }) {
|
|
|
1290
1431
|
return rest.length === 0 ? [first] : [first, ...(await moveMessages(rest, first.folderUid))];
|
|
1291
1432
|
}
|
|
1292
1433
|
/**
|
|
1293
|
-
* This mailbox's folder of `type
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1434
|
+
* This mailbox's folder of `type`. The server makes every well-known folder itself (when the mailbox is made, and again when the folders are listed
|
|
1435
|
+
* if one is missing), so a folder the page's tree lacks is asked for - the tree may simply be out of date - and used, never created a second time: a
|
|
1436
|
+
* duplicate of a well-known type is invisible to the server (it answers with the oldest) but would be a second row in the sidebar. Only a server that
|
|
1437
|
+
* really has none (an older one, which makes Deleted Items and Junk on first use) is asked to create it.
|
|
1296
1438
|
*
|
|
1297
|
-
* A folder created here
|
|
1298
|
-
*
|
|
1439
|
+
* A folder found or created here may not be in `mailboxFolders` yet, so it is filed there and remembered per mailbox and type until the page
|
|
1440
|
+
* reloads; otherwise a second Delete would ask again.
|
|
1299
1441
|
*/
|
|
1300
1442
|
async function resolveFolderOfType(type, name) {
|
|
1301
1443
|
const key = `${activeMailboxUid}:${type}`;
|
|
@@ -1303,6 +1445,12 @@ function InboxContent({ userUid }) {
|
|
|
1303
1445
|
if (known) {
|
|
1304
1446
|
return known;
|
|
1305
1447
|
}
|
|
1448
|
+
const listed = (await listFolders(activeMailboxUid)).find((f) => f.type === type);
|
|
1449
|
+
if (listed) {
|
|
1450
|
+
onFolderCreated(listed);
|
|
1451
|
+
lazyFoldersRef.current.set(key, listed.uid);
|
|
1452
|
+
return listed.uid;
|
|
1453
|
+
}
|
|
1306
1454
|
const created = await createFolder({ mailboxUid: activeMailboxUid, name, type });
|
|
1307
1455
|
lazyFoldersRef.current.set(key, created.uid);
|
|
1308
1456
|
return created.uid;
|
|
@@ -1321,8 +1469,8 @@ function InboxContent({ userUid }) {
|
|
|
1321
1469
|
return { uid: message.uid, version: message.version, labelUids: [...new Set([...labelUids, ...kept])] };
|
|
1322
1470
|
})), false);
|
|
1323
1471
|
}
|
|
1324
|
-
function moveSelectionToType(type, name) {
|
|
1325
|
-
|
|
1472
|
+
function moveSelectionToType(type, name, chosen) {
|
|
1473
|
+
return runBulkAction(async (moving) => moveMessages(moving, await resolveFolderOfType(type, name)), true, chosen);
|
|
1326
1474
|
}
|
|
1327
1475
|
const loadMoreStatus = loadingMore ? ("Loading more\u2026") : loadMoreError ? (_jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { role: "alert", className: "text-danger", children: loadMoreError }), _jsx("button", { type: "button", onClick: () => void loadMoreRef.current(), className: "text-primary-dark hover:underline font-medium", children: "Retry" })] })) : loadMoreStalled ? (_jsx("button", { type: "button", onClick: () => void loadMoreRef.current(), className: "text-primary-dark hover:underline font-medium", children: "Load more" })) : null;
|
|
1328
1476
|
function handleSelect(message) {
|
|
@@ -1331,9 +1479,10 @@ function InboxContent({ userUid }) {
|
|
|
1331
1479
|
return;
|
|
1332
1480
|
}
|
|
1333
1481
|
if (isMobile) {
|
|
1334
|
-
|
|
1482
|
+
navigate(`/messages/${encodeURIComponent(message.uid)}`);
|
|
1335
1483
|
return;
|
|
1336
1484
|
}
|
|
1485
|
+
removedAnchorRef.current = null;
|
|
1337
1486
|
setSelectedUid(message.uid);
|
|
1338
1487
|
}
|
|
1339
1488
|
/** Opens a conversation in the reading pane, positioned at one of its messages: the one a child row
|
|
@@ -1348,12 +1497,142 @@ function InboxContent({ userUid }) {
|
|
|
1348
1497
|
if (isMobile) {
|
|
1349
1498
|
// No dedicated mobile thread route yet - the existing single-message detail route already
|
|
1350
1499
|
// handles any message uid regardless of conversation grouping.
|
|
1351
|
-
|
|
1500
|
+
navigate(`/messages/${encodeURIComponent(uid)}`);
|
|
1352
1501
|
return;
|
|
1353
1502
|
}
|
|
1503
|
+
removedAnchorRef.current = null;
|
|
1354
1504
|
setSelectedUid(uid);
|
|
1355
1505
|
setOpenThread({ conversation, uid });
|
|
1356
1506
|
}
|
|
1507
|
+
// ---- Keyboard shortcuts (see `shared/keyboard`). Each is registered only while this view can do it, and calls what the toolbar and
|
|
1508
|
+
// ---- the selection bar call: the same bulk-action path (`runBulkAction()` - so the same optimistic badge tracking, the same rollback,
|
|
1509
|
+
// ---- the same lazily created Deleted Items folder), never a second implementation. Reply, Reply all, Forward, Archive and Move to are
|
|
1510
|
+
// ---- registered by the reading pane itself (`MessageDetailPane`'s `shortcuts`), which owns those handlers.
|
|
1511
|
+
const inConversations = preferences.showAsConversations;
|
|
1512
|
+
/** The selection bar isn't offered in the aggregate view (its rows belong to several mailboxes), and neither are these. */
|
|
1513
|
+
const keyboardActions = !aggregateFolderType;
|
|
1514
|
+
/** What the keyboard acts on exists: the ticked rows in select mode, else the message (or conversation) open in the reading pane. */
|
|
1515
|
+
const keyboardTargetExists = selectMode ? selectedMessages.length > 0 : inConversations ? openThread !== null : selected !== null;
|
|
1516
|
+
const rowCount = inConversations ? listedConversations.length : messages.length;
|
|
1517
|
+
const canMoveSelection = !selectMode && !isMobile && !loading && rowCount > 0;
|
|
1518
|
+
function folderTypeOf(folderUidOfMessage) {
|
|
1519
|
+
return currentFolders.find((f) => f.uid === folderUidOfMessage)?.type;
|
|
1520
|
+
}
|
|
1521
|
+
/** Moves the keyboard's focus to a row - and scrolls it into view - once the selection has moved to it. */
|
|
1522
|
+
function focusRow(uid) {
|
|
1523
|
+
// Only called from a key press, when the list is on screen.
|
|
1524
|
+
const row = [...scrollContainerRef.current.querySelectorAll("[data-message-uid]")].find((element) => element.getAttribute("data-message-uid") === uid);
|
|
1525
|
+
row?.scrollIntoView({ block: "nearest" });
|
|
1526
|
+
row?.querySelector("[data-row-open]")?.focus({ preventScroll: true });
|
|
1527
|
+
}
|
|
1528
|
+
/** The index of the selected row: the open message's, or the open conversation's; -1 with nothing selected. */
|
|
1529
|
+
function selectedRowIndex() {
|
|
1530
|
+
if (inConversations) {
|
|
1531
|
+
return openThread ? listedConversations.findIndex((c) => c.conversationId === openThread.conversation.conversationId) : -1;
|
|
1532
|
+
}
|
|
1533
|
+
return messages.findIndex((m) => m.uid === selectedUid);
|
|
1534
|
+
}
|
|
1535
|
+
function selectRow(index) {
|
|
1536
|
+
if (inConversations) {
|
|
1537
|
+
const conversation = listedConversations[index];
|
|
1538
|
+
handleOpenConversation(conversation, conversation.latestMessageUid);
|
|
1539
|
+
focusRow(conversation.latestMessageUid);
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
const message = messages[index];
|
|
1543
|
+
handleSelect(message);
|
|
1544
|
+
focusRow(message.uid);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function selectNeighbour(step) {
|
|
1548
|
+
const current = selectedRowIndex();
|
|
1549
|
+
// After the selected message left the list (deleted, archived) the next row is the one that slid into its place.
|
|
1550
|
+
const anchor = removedAnchorRef.current;
|
|
1551
|
+
const target = current === -1 && anchor !== null ? (step === 1 ? anchor : anchor - 1) : current + step;
|
|
1552
|
+
selectRow(Math.min(rowCount - 1, Math.max(0, target)));
|
|
1553
|
+
}
|
|
1554
|
+
function selectNextUnread(step) {
|
|
1555
|
+
const isRowUnread = (index) => (inConversations ? listedConversations[index].unreadCount > 0 : isUnread(messages[index]));
|
|
1556
|
+
for (let index = selectedRowIndex() + step; index >= 0 && index < rowCount; index += step) {
|
|
1557
|
+
if (isRowUnread(index)) {
|
|
1558
|
+
selectRow(index);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
/** The messages of the open conversation that are in the folder being listed - fetched (once) like a ticked conversation's. */
|
|
1564
|
+
async function loadOpenConversation(conversation) {
|
|
1565
|
+
const id = conversation.conversationId;
|
|
1566
|
+
let all = conversationMessagesById[id];
|
|
1567
|
+
if (!all) {
|
|
1568
|
+
all = await listConversationMessages(activeMailboxUid, id);
|
|
1569
|
+
const loaded = all;
|
|
1570
|
+
setConversationMessagesById((prev) => ({ ...prev, [id]: loaded }));
|
|
1571
|
+
}
|
|
1572
|
+
return all.filter((m) => !folderUid || m.folderUid === folderUid);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Runs a bulk action on what the keyboard acts on - the ticked rows in select mode, else the open message or conversation - keeping only
|
|
1576
|
+
* the messages `applies` accepts (Delete leaves out what is already in Deleted Items, Mark read what is already read: nothing to do
|
|
1577
|
+
* then). Held while another bulk action is on the wire, like the bar's buttons. Closes the open thread after a move away from it.
|
|
1578
|
+
*/
|
|
1579
|
+
async function runOnKeyboardTargets(action, removesRows, applies = () => true) {
|
|
1580
|
+
if (bulkBusy || resolvingSelection > 0) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
let chosen;
|
|
1584
|
+
try {
|
|
1585
|
+
chosen = selectMode ? selectedMessages : inConversations ? await loadOpenConversation(openThread.conversation) : [selected];
|
|
1586
|
+
}
|
|
1587
|
+
catch (err) {
|
|
1588
|
+
notifyApiError(err, "Couldn't load the messages in that conversation");
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
chosen = chosen.filter(applies);
|
|
1592
|
+
if (chosen.length === 0) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const succeeded = await runBulkAction(action, removesRows, chosen);
|
|
1596
|
+
if (succeeded && removesRows && inConversations && !selectMode) {
|
|
1597
|
+
setOpenThread(null);
|
|
1598
|
+
setSelectedUid(null);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
useShortcut(SHORTCUTS.mail.next, () => selectNeighbour(1), { enabled: canMoveSelection });
|
|
1602
|
+
useShortcut(SHORTCUTS.mail.previous, () => selectNeighbour(-1), { enabled: canMoveSelection });
|
|
1603
|
+
useShortcut(SHORTCUTS.mail.nextUnread, () => selectNextUnread(1), { enabled: canMoveSelection });
|
|
1604
|
+
useShortcut(SHORTCUTS.mail.previousUnread, () => selectNextUnread(-1), { enabled: canMoveSelection });
|
|
1605
|
+
useShortcut(SHORTCUTS.mail.delete, () => void runOnKeyboardTargets(async (chosen) => moveMessages(chosen, await resolveFolderOfType("deleted_items", "Deleted Items")), true, (message) => folderTypeOf(message.folderUid) !== "deleted_items"), { enabled: keyboardActions && keyboardTargetExists });
|
|
1606
|
+
useShortcut(SHORTCUTS.mail.markRead, () => void runOnKeyboardTargets((chosen) => setReadStateMany(chosen, true, { patch: patchListedMessage, track: trackMessageChange }), false, isUnread), { enabled: keyboardActions && keyboardTargetExists });
|
|
1607
|
+
useShortcut(SHORTCUTS.mail.markUnread, () => void runOnKeyboardTargets((chosen) => setReadStateMany(chosen, false, { patch: patchListedMessage, track: trackMessageChange }), false, (message) => !isUnread(message)), { enabled: keyboardActions && keyboardTargetExists });
|
|
1608
|
+
useShortcut(SHORTCUTS.mail.flag, () =>
|
|
1609
|
+
// Flags them all unless every one already is - what the bar's Flag and Unflag would do on this selection.
|
|
1610
|
+
void runOnKeyboardTargets((chosen) => setMessagesFlagged(chosen, !chosen.every((m) => m.flags.flagged)), false), { enabled: keyboardActions && keyboardTargetExists });
|
|
1611
|
+
useShortcut(SHORTCUTS.mail.open, (event) => {
|
|
1612
|
+
// Enter on a row that is not selected yet is the row button's own click (it selects). On the selected one - or with the focus
|
|
1613
|
+
// elsewhere - it opens the message on its own page, as it does in Outlook.
|
|
1614
|
+
const rowUid = event.target.closest("[data-message-uid]")?.getAttribute("data-message-uid");
|
|
1615
|
+
if (rowUid ? rowUid !== selectedUid : isActivatable(event.target)) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
navigate(`/messages/${encodeURIComponent(selectedUid)}`);
|
|
1619
|
+
}, { enabled: !selectMode && selectedUid !== null });
|
|
1620
|
+
useShortcut(SHORTCUTS.mail.close, () => {
|
|
1621
|
+
if (document.activeElement === searchInputRef.current && searchInput !== "") {
|
|
1622
|
+
setSearchInput("");
|
|
1623
|
+
}
|
|
1624
|
+
else if (selectMode) {
|
|
1625
|
+
leaveSelectMode();
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
setSelectedUid(null);
|
|
1629
|
+
setOpenThread(null);
|
|
1630
|
+
}
|
|
1631
|
+
}, { enabled: selectMode || selectedUid !== null || openThread !== null || searchInput !== "" });
|
|
1632
|
+
useShortcut(SHORTCUTS.mail.search, () => {
|
|
1633
|
+
searchInputRef.current?.focus();
|
|
1634
|
+
searchInputRef.current?.select();
|
|
1635
|
+
}, { enabled: !inConversations && !aggregateFolderType });
|
|
1357
1636
|
if (!folderUid && !aggregateFolderType) {
|
|
1358
1637
|
// `MailShell` never renders this component at all until a mailbox is resolved (see its own
|
|
1359
1638
|
// full-screen `MailboxProvisioning` takeover otherwise) — this is purely the brief gap before
|
|
@@ -1362,11 +1641,18 @@ function InboxContent({ userUid }) {
|
|
|
1362
1641
|
// indistinguishable to anything (a test, a user re-reading the screen) that catches this one.
|
|
1363
1642
|
return _jsx("p", { className: "p-8 text-sm text-text-muted", children: "Loading your mailbox\u2026" });
|
|
1364
1643
|
}
|
|
1365
|
-
return (_jsxs("div", { className: "flex h-full min-h-0", children: [_jsxs("div", { ref: scrollContainerRef,
|
|
1644
|
+
return (_jsxs("div", { className: "flex h-full min-h-0", children: [_jsxs("div", { ref: scrollContainerRef,
|
|
1645
|
+
// Remembered as it moves (not when the list is left: by then React has already detached the element), so
|
|
1646
|
+
// the folder - or Mail, after another app - comes back where it was.
|
|
1647
|
+
onScroll: (event) => saveListScroll(listKey, event.currentTarget.scrollTop), className: "w-full md:w-96 shrink-0 md:border-r border-border overflow-y-auto", children: [selectMode ? (_jsx(MailSelectionBar, { selected: selectedMessages, listed: messages, totals: preferences.showAsConversations
|
|
1366
1648
|
? { selected: selectedConversationIds.size, listed: conversations.length, noun: "conversation" }
|
|
1367
1649
|
: undefined, onSelectAll: () => preferences.showAsConversations
|
|
1368
1650
|
? selectAllConversations()
|
|
1369
|
-
: setSelectedUids(new Set(messages.map((m) => m.uid))), onClearSelection: () => preferences.showAsConversations ? setSelectedConversationIds(new Set()) : setSelectedUids(new Set()), onCancel: leaveSelectMode, folders: currentFolders, currentFolderUid: folderUid, labels: mailboxLabels, mailboxUid: activeMailboxUid, onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated, onApplyLabels: applyLabelsToSelection, onSetRead: (read) => void runBulkAction(
|
|
1651
|
+
: setSelectedUids(new Set(messages.map((m) => m.uid))), onClearSelection: () => preferences.showAsConversations ? setSelectedConversationIds(new Set()) : setSelectedUids(new Set()), onCancel: leaveSelectMode, folders: currentFolders, currentFolderUid: folderUid, labels: mailboxLabels, mailboxUid: activeMailboxUid, onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated, onApplyLabels: applyLabelsToSelection, onSetRead: (read) => void runBulkAction(
|
|
1652
|
+
// Optimistic: the rows and the folder badges change now, and change back if the server refuses.
|
|
1653
|
+
(chosen) => setReadStateMany(chosen, read, { patch: patchListedMessage, track: trackMessageChange }), false), onSetFlagged: (flagged) => void runBulkAction((chosen) => setMessagesFlagged(chosen, flagged), false), shortcuts: keyboardActions, onArchive: () => void runBulkAction(bulkArchive, true), onMoveTo: async (targetFolderUid) => {
|
|
1654
|
+
await runBulkAction((chosen) => moveMessages(chosen, targetFolderUid), true);
|
|
1655
|
+
}, onReportJunk: () => void moveSelectionToType("junk", "Junk Email"), onDelete: () => void moveSelectionToType("deleted_items", "Deleted Items"), busy: bulkBusy || resolvingSelection > 0 })) : (_jsx(MailListToolbar, { sortBy: preferences.sortBy, sortOrder: preferences.sortOrder, filter: preferences.filter, labelUids: preferences.labelUids, labels: mailboxLabels, mailboxUid: activeMailboxUid, onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onLabelUidsChange: (nextLabelUids) => updatePreferences({ labelUids: nextLabelUids }), showAsConversations: preferences.showAsConversations, onSortChange: (sortBy, sortOrder) => updatePreferences({ sortBy, sortOrder }), onFilterChange: (filter) => updatePreferences({ filter }), onShowAsConversationsChange: (showAsConversations) => updatePreferences({ showAsConversations }), selectMode: selectMode, onSelectModeChange: setSelectMode, offerClassificationFilters: offerClassificationFilters, filterDisabled: isSearching, filterDisabledReason: "Filters don't apply to search results", sortKeysDisabled: isSearching || !!aggregateFolderType,
|
|
1370
1656
|
// A conversation row is a thread summary, so only some of the keys have anything to
|
|
1371
1657
|
// order by - the rest stay pickable and are applied to the rows already fetched.
|
|
1372
1658
|
unavailableSortKeys: preferences.showAsConversations ? CONVERSATION_SORT_UNAVAILABLE : undefined, sortKeysNote: isSearching
|
|
@@ -1379,7 +1665,7 @@ function InboxContent({ userUid }) {
|
|
|
1379
1665
|
? "Open a mailbox's own folder to select messages"
|
|
1380
1666
|
: loading
|
|
1381
1667
|
? "Wait for this folder to finish loading"
|
|
1382
|
-
: "There is nothing here to select" })), !preferences.showAsConversations && (_jsx("div", { className: "p-2 border-b border-border", children: _jsx("input", { type: "search", value: searchInput, onChange: (e) => setSearchInput(e.target.value), placeholder: aggregateFolderType ? "Open a mailbox's own folder to search" : "Search all mail…", "aria-label": "Search all mail", disabled: !!aggregateFolderType, className: "w-full text-sm px-3 py-1.5 rounded-md border border-border bg-surface disabled:opacity-55" }) })), offerClassificationFilters && (_jsx("div", { className: "flex border-b border-border text-xs", children: MAIL_LIST_CLASSIFICATION_FILTERS.map(({ value, label }) => (_jsx("button", { type: "button", "aria-pressed": preferences.filter === value, onClick: () => updatePreferences({ filter: value }), className: [
|
|
1668
|
+
: "There is nothing here to select" })), !preferences.showAsConversations && (_jsx("div", { className: "p-2 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: searchInput, onChange: (e) => setSearchInput(e.target.value), placeholder: aggregateFolderType ? "Open a mailbox's own folder to search" : "Search all mail…", "aria-label": "Search all mail", disabled: !!aggregateFolderType, className: "w-full text-sm px-3 py-1.5 rounded-md border border-border bg-surface disabled:opacity-55" }) })), offerClassificationFilters && (_jsx("div", { className: "flex border-b border-border text-xs", children: MAIL_LIST_CLASSIFICATION_FILTERS.map(({ value, label }) => (_jsx("button", { type: "button", "aria-pressed": preferences.filter === value, onClick: () => updatePreferences({ filter: value }), className: [
|
|
1383
1669
|
"flex-1 py-1.5 font-semibold",
|
|
1384
1670
|
preferences.filter === value
|
|
1385
1671
|
? "text-primary-dark border-b-2 border-primary-dark"
|
|
@@ -1388,17 +1674,14 @@ function InboxContent({ userUid }) {
|
|
|
1388
1674
|
? `${messages.length} result${messages.length === 1 ? "" : "s"}`
|
|
1389
1675
|
: `${messages.length} of ??` }), tier2Done && !searchAllMail && (_jsx("button", { type: "button", onClick: handleSearchAllMail, className: "text-primary-dark hover:underline font-medium shrink-0", children: "Search all mail" }))] })), isSearching && coverage?.indexedFrom && (_jsxs("div", { className: "px-4 py-1.5 text-xs text-text-muted border-b border-border", children: ["Local search covers messages back to ", new Date(coverage.indexedFrom).toLocaleDateString(), coverage.building ? " (still building)" : "", searchAllMail
|
|
1390
1676
|
? " - searching everything, not just recent mail."
|
|
1391
|
-
: " - older encrypted mail is still searched, just slower."] })), isSearching && !getUnlockedKeys(mailboxUid) && (_jsx("div", { className: "px-4 py-2 border-b border-border bg-surface-alt", children: _jsxs("button", { type: "button", onClick: handleUnlockSearch, className: "inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to include encrypted messages in these results"] }) })), !isSearching && !preferences.showAsConversations && undecryptedEncryptedUids.length > 0 && !getUnlockedKeys(activeMailboxUid) && (_jsx("div", { className: "px-4 py-2 border-b border-border bg-surface-alt", children: _jsxs("button", { type: "button", onClick: handleUnlockList, className: "inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to show ", undecryptedEncryptedUids.length === 1 ? "an encrypted message's" : "encrypted messages'", " subject"] }) })), error && (_jsx("div", { className: "p-4", children: _jsx(Alert, { children: error }) })), loading ? (
|
|
1677
|
+
: " - older encrypted mail is still searched, just slower."] })), isSearching && !getUnlockedKeys(mailboxUid) && (_jsx("div", { className: "px-4 py-2 border-b border-border bg-surface-alt", children: _jsxs("button", { type: "button", onClick: handleUnlockSearch, className: "inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to include encrypted messages in these results"] }) })), !isSearching && !preferences.showAsConversations && undecryptedEncryptedUids.length > 0 && !getUnlockedKeys(activeMailboxUid) && (_jsx("div", { className: "px-4 py-2 border-b border-border bg-surface-alt", children: _jsxs("button", { type: "button", onClick: handleUnlockList, className: "inline-flex items-center gap-1 text-xs font-medium text-primary-dark hover:underline", children: [_jsx(HiOutlineLockClosed, { size: 12, "aria-hidden": "true" }), "Unlock to show ", undecryptedEncryptedUids.length === 1 ? "an encrypted message's" : "encrypted messages'", " subject"] }) })), error && (_jsx("div", { className: "p-4", children: _jsx(Alert, { children: error }) })), loading ? (_jsxs("div", { role: "status", "aria-busy": "true", className: "p-4", children: [_jsx("span", { className: "sr-only", children: "Loading\u2026" }), _jsx(SkeletonList, { count: 8 })] })) : preferences.showAsConversations ? (_jsxs(_Fragment, { children: [_jsx(ConversationList, { conversations: listedConversations, newestFirst: preferences.sortBy === "date" && preferences.sortOrder === "desc", mailboxUid: activeMailboxUid, selectedUid: selectedUid, messageOverrides: conversationPatches, onOpenMessage: handleOpenConversation, selectMode: selectMode, selectedConversationIds: selectedConversationIds, onToggleSelected: toggleConversationSelected }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus }))] })) : messages.length === 0 ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "p-4 text-sm text-text-muted", children: isSearching
|
|
1392
1678
|
? `No messages match "${searchQuery}".`
|
|
1393
1679
|
: effectiveFilter === "all"
|
|
1394
1680
|
? "No messages in this folder."
|
|
1395
|
-
: "No messages here." }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus }))] })) : (_jsxs(_Fragment, { children: [_jsx("ul", { children: messages.map((message) => (_jsxs("li", { className: "flex items-stretch
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
: "hover:bg-surface-alt",
|
|
1400
|
-
message.flags.read ? "" : "font-semibold",
|
|
1401
|
-
].join(" "), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: message.from }), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(message.receivedDate).toLocaleDateString() })] }), aggregateFolderType && (_jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: mailboxes.find((mb) => mb.uid === message.mailboxUid)?.displayName })), isSearching && pendingUids.has(message.uid) ? (_jsxs("div", { className: "flex flex-col gap-1.5 py-0.5", children: [_jsx(Skeleton, { height: "h-3.5", className: "w-2/3 rounded-sm" }), _jsx(Skeleton, { height: "h-3", className: "w-full rounded-sm" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-sm truncate", children: rowSubject(message) }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-muted font-normal", children: [_jsx("span", { className: "truncate", children: snippets[message.uid] || decryptedRows[message.uid]?.preview || message.bodyPreview }), message.hasAttachments && _jsx(HiOutlinePaperClip, { size: 12, "aria-label": "Has attachments" }), message.flags.flagged && (_jsx(HiOutlineFlag, { size: 12, "aria-label": "Flagged", className: "text-danger" }))] })] }))] })] }, message.uid))) }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus })), aggregateFolderType && (_jsx("p", { className: "p-4 text-center text-xs text-text-muted", children: "Showing the most recent mail from each mailbox. Open a specific mailbox\u2019s folder to see older mail." }))] }))] }), _jsx("div", { className: "hidden md:flex flex-1 min-w-0 min-h-0", children: preferences.showAsConversations ? (_jsx(ConversationThreadPane, { conversation: openThread?.conversation ?? null, selectedUid: openThread?.uid ?? null, mailboxUid: activeMailboxUid, folders: currentFolders, labels: mailboxLabels, onMessagePatched: patchListedMessage, onMessageRemoved: (updated) => removeListedMessage(updated.uid), onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated })) : (_jsx(MessageDetailPane, { message: selected, attachments: attachments, isSentItems: isSentItems, onRecalled: patchListedMessage, isOutbox: isOutbox, onReceiptHandled: patchListedMessage, draftsFolderUid: draftsFolderUid, folders: folders, onMoved: (updated) => {
|
|
1681
|
+
: "No messages here." }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus }))] })) : (_jsxs(_Fragment, { children: [_jsx("ul", { children: messages.map((message) => (_jsxs("li", { "data-message-uid": message.uid, "data-unread": isUnread(message) ? "true" : undefined, className: rowClass({ unread: isUnread(message), selected: message.uid === selectedUid || selectedUids.has(message.uid) }, "flex items-stretch"), children: [_jsx(UnreadBar, { unread: isUnread(message) }), selectMode && (_jsx("span", { className: "shrink-0 flex items-center pl-3", children: _jsx("input", { type: "checkbox", checked: selectedUids.has(message.uid), onChange: () => toggleSelected(message.uid), "aria-label": `Select ${rowSubject(message)}`, className: "w-4 h-4 accent-primary" }) })), _jsxs("button", { type: "button", "data-row-open": true, onClick: () => handleSelect(message), className: ["flex-1 min-w-0 text-left px-4 py-3", ROW_FOCUS_CLASS].join(" "), children: [_jsx(UnreadLabel, { unread: isUnread(message) }), _jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: message.from, className: senderClass(isUnread(message)) }), _jsx("span", { className: ["text-xs shrink-0", dateClass(isUnread(message))].join(" "), children: new Date(message.receivedDate).toLocaleDateString() })] }), aggregateFolderType && (_jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: mailboxes.find((mb) => mb.uid === message.mailboxUid)?.displayName })), isSearching && pendingUids.has(message.uid) ? (_jsxs("div", { className: "flex flex-col gap-1.5 py-0.5", children: [_jsx(Skeleton, { height: "h-3.5", className: "w-2/3 rounded-sm" }), _jsx(Skeleton, { height: "h-3", className: "w-full rounded-sm" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: ["text-sm truncate", subjectClass(isUnread(message))].join(" "), children: rowSubject(message) }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-muted font-normal", children: [_jsx("span", { className: "truncate", children: snippets[message.uid] ||
|
|
1682
|
+
decryptedRows[message.uid]?.preview ||
|
|
1683
|
+
message.bodyPreview ||
|
|
1684
|
+
(message.encrypted ? _jsx(EncryptedPreview, {}) : null) }), message.hasAttachments && _jsx(HiOutlinePaperClip, { size: 12, "aria-label": "Has attachments" }), message.flags.flagged && (_jsx(HiOutlineFlag, { size: 12, "aria-label": "Flagged", className: "text-danger" }))] }), isOutbox && _jsx(OutboxRowStatus, { message: message })] }))] })] }, message.uid))) }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus })), aggregateFolderType && (_jsx("p", { className: "p-4 text-center text-xs text-text-muted", children: "Showing the most recent mail from each mailbox. Open a specific mailbox\u2019s folder to see older mail." }))] }))] }), _jsx("div", { className: "hidden md:flex flex-1 min-w-0 min-h-0", children: preferences.showAsConversations ? (_jsx(LazyConversationThreadPane, { conversation: openThread?.conversation ?? null, selectedUid: openThread?.uid ?? null, mailboxUid: activeMailboxUid, folders: currentFolders, labels: mailboxLabels, shortcuts: !selectMode, onMessagePatched: patchListedMessage, onMessageRemoved: (updated) => removeListedMessage(updated.uid), onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated })) : (_jsx(LazyMessageDetailPane, { shortcuts: !selectMode, message: selected, attachments: attachments, isSentItems: isSentItems, onRecalled: patchListedMessage, isOutbox: isOutbox, onReceiptHandled: patchListedMessage, draftsFolderUid: draftsFolderUid, folders: folders, onMoved: (updated) => {
|
|
1402
1685
|
// Same reasoning as onArchived below - a move takes the message out of the folder
|
|
1403
1686
|
// being listed, so it leaves the list rather than being patched in place.
|
|
1404
1687
|
removeListedMessage(updated.uid);
|
|
@@ -1414,3 +1697,4 @@ function InboxContent({ userUid }) {
|
|
|
1414
1697
|
removeListedMessage(updated.uid);
|
|
1415
1698
|
}, labels: labels, onLabelsChanged: patchListedMessage, onLabelCreated: (label) => (otherMailbox ? setOtherMailboxLabels : setMailboxLabels)((prev) => [...prev, label]) })) })] }));
|
|
1416
1699
|
}
|
|
1700
|
+
export default routedPage("/", InboxPage);
|