@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,308 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { Folder, FolderType, Mailbox, Message, listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The folder badges' numbers, kept right without a reload.
|
|
10
|
+
*
|
|
11
|
+
* A folder's `unreadCount`/`totalCount` come from the server (`GET /mail/folders`, the server-published `Folder` update
|
|
12
|
+
* event, and the poll), but waiting for a round trip to see a badge move after reading a message feels broken, so a change
|
|
13
|
+
* made here is applied to the badge at once - **optimistically** - and then *reconciled*: every so often, and after every
|
|
14
|
+
* change, the real counts are read back and replace whatever this page worked out. So the page can be wrong for a moment,
|
|
15
|
+
* never for good.
|
|
16
|
+
*
|
|
17
|
+
* What the server sends (`@rapidmx/restapi`): `GET /mail/folders` returns each folder with a derived `unreadCount`/`totalCount`
|
|
18
|
+
* (unread messages, all messages), and after any change to a folder's messages it publishes
|
|
19
|
+
* `{ type: /^Folder/, action: "update", data: { uid, mailboxUid, unreadCount, totalCount } }` on the folder's push channel *and* its
|
|
20
|
+
* mailbox's, so each is heard twice - the values are absolute and applying one again changes nothing. Nothing here fails without
|
|
21
|
+
* that event, or for a write that publishes none (ActiveSync/MAPI, erasure jobs): the read-back after each change and the poll do
|
|
22
|
+
* the same job a little later, and overlapping writes to one folder are unordered, so they always have the last word.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface FolderCount {
|
|
26
|
+
unread: number;
|
|
27
|
+
total: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A change to one folder's counts. */
|
|
31
|
+
export interface CountDelta {
|
|
32
|
+
folderUid: string;
|
|
33
|
+
unread: number;
|
|
34
|
+
total: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What one message change - a new message (`previous` absent), a deletion (`next` absent), a read/unread flip, a move - does to the folders' counts. */
|
|
38
|
+
export function countDeltas(previous: Message | null | undefined, next: Message | null | undefined): CountDelta[] {
|
|
39
|
+
const byFolder = new Map<string, CountDelta>();
|
|
40
|
+
function add(message: Message, sign: 1 | -1) {
|
|
41
|
+
const entry = byFolder.get(message.folderUid) ?? { folderUid: message.folderUid, unread: 0, total: 0 };
|
|
42
|
+
entry.total += sign;
|
|
43
|
+
entry.unread += message.flags.read === true ? 0 : sign;
|
|
44
|
+
byFolder.set(message.folderUid, entry);
|
|
45
|
+
}
|
|
46
|
+
if (previous) {
|
|
47
|
+
add(previous, -1);
|
|
48
|
+
}
|
|
49
|
+
if (next) {
|
|
50
|
+
add(next, 1);
|
|
51
|
+
}
|
|
52
|
+
return [...byFolder.values()].filter((delta) => delta.unread !== 0 || delta.total !== 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A folder's count as it should be shown: what this page last worked out or was told, else what the folder was listed with. */
|
|
56
|
+
export function countOfFolder(folder: Folder, counts: Record<string, FolderCount>): FolderCount {
|
|
57
|
+
return counts[folder.uid] ?? { unread: folder.unreadCount, total: folder.totalCount };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What a folder's badge shows: how many unread, or (Drafts and Outbox, Outlook-style) how many messages are in it. */
|
|
61
|
+
export interface FolderBadge {
|
|
62
|
+
kind: "unread" | "total";
|
|
63
|
+
value: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The badge a folder gets, if any. Inbox, Archive and the user's own folders show their unread count when it is above
|
|
68
|
+
* zero; Drafts and Outbox show how many messages they hold (an unread count means nothing there); Sent Items, Deleted
|
|
69
|
+
* Items and Junk Email show nothing.
|
|
70
|
+
*/
|
|
71
|
+
export function badgeFor(type: FolderType, count: FolderCount): FolderBadge | undefined {
|
|
72
|
+
switch (type) {
|
|
73
|
+
case "sent_items":
|
|
74
|
+
case "deleted_items":
|
|
75
|
+
case "junk":
|
|
76
|
+
return undefined;
|
|
77
|
+
case "drafts":
|
|
78
|
+
case "outbox":
|
|
79
|
+
return count.total > 0 ? { kind: "total", value: count.total } : undefined;
|
|
80
|
+
default:
|
|
81
|
+
return count.unread > 0 ? { kind: "unread", value: count.unread } : undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The badge's text for assistive technology: "3 unread", "2 messages". */
|
|
86
|
+
export function badgeLabel(badge: FolderBadge): string {
|
|
87
|
+
return badge.kind === "unread" ? `${badge.value} unread` : `${badge.value} ${badge.value === 1 ? "message" : "messages"}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The unread messages in every mailbox's Inbox - what the tab title counts. */
|
|
91
|
+
export function inboxUnreadTotal(mailboxFolders: { folders: Folder[] }[], counts: Record<string, FolderCount>): number {
|
|
92
|
+
return mailboxFolders.reduce(
|
|
93
|
+
(total, entry) =>
|
|
94
|
+
total + entry.folders.filter((folder) => folder.type === "inbox").reduce((sum, folder) => sum + countOfFolder(folder, counts).unread, 0),
|
|
95
|
+
0,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** How long after the last change of this page's own has finished a server-published count is trusted again: one published
|
|
100
|
+
* about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
|
|
101
|
+
export const COUNT_QUIET_MS = 1_500;
|
|
102
|
+
|
|
103
|
+
/** How long after a change the counts are read back from the server. */
|
|
104
|
+
export const COUNT_REFRESH_DELAY_MS = 600;
|
|
105
|
+
|
|
106
|
+
/** How many message uids the "already counted this new message" memory holds. */
|
|
107
|
+
const SEEN_LIMIT = 500;
|
|
108
|
+
|
|
109
|
+
/** An optimistic change in flight: call exactly one of these once the server has answered. */
|
|
110
|
+
export interface CountTracker {
|
|
111
|
+
/** The server accepted the change: keep what was applied, and read the real counts back. */
|
|
112
|
+
settle(): void;
|
|
113
|
+
/** The server refused it (or it failed): put the counts back as they were, and read the real ones back. */
|
|
114
|
+
revert(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface FolderCounts {
|
|
118
|
+
/** The counts this page has worked out or been told since load, by folder uid - see `countOfFolder()`. */
|
|
119
|
+
counts: Record<string, FolderCount>;
|
|
120
|
+
/** Applies the effect of changing `previous` into `next` now, before the server has answered - see `CountTracker`. */
|
|
121
|
+
track(previous: Message | null | undefined, next: Message | null | undefined): CountTracker;
|
|
122
|
+
/** A new message arrived over the push connection: counts it (once per message) until the real counts are read back.
|
|
123
|
+
* `false` when this one was already counted - a duplicate event. */
|
|
124
|
+
noteCreated(message: Message): boolean;
|
|
125
|
+
/** The server's `Folder` update event's `data`: the folder's real counts - when this page has nothing of its own in flight. */
|
|
126
|
+
applyFolderEvent(data: unknown): void;
|
|
127
|
+
/** Reads every mailbox's real counts back and shows them, after `delayMs` (at once for 0). A later call replaces one still waiting. */
|
|
128
|
+
refresh(delayMs?: number): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Holds the folder counts overlay for `MailShell`. `folders` is every folder the sidebar lists (the overlay starts from what
|
|
133
|
+
* each was loaded with); `mailboxes` is whose folders to read back. The overlay starts again whenever `mailboxes` does.
|
|
134
|
+
*/
|
|
135
|
+
export function useFolderCounts(mailboxes: Mailbox[], folders: Folder[], onFoldersListed?: (folders: Folder[]) => void): FolderCounts {
|
|
136
|
+
const [counts, setCounts] = useState<Record<string, FolderCount>>({});
|
|
137
|
+
const latestRef = useRef({ mailboxes, folders, onFoldersListed });
|
|
138
|
+
latestRef.current = { mailboxes, folders, onFoldersListed };
|
|
139
|
+
// Bumped by every change of the overlay this page makes, so a read that started before one can tell it is out of date.
|
|
140
|
+
const changesRef = useRef(0);
|
|
141
|
+
const inFlightRef = useRef(0);
|
|
142
|
+
const lastDoneRef = useRef(0);
|
|
143
|
+
const runRef = useRef(0);
|
|
144
|
+
// Assigned during every render, before anything can call it.
|
|
145
|
+
const readBackRef = useRef<() => Promise<void>>(undefined as never);
|
|
146
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
147
|
+
const stoppedRef = useRef(false);
|
|
148
|
+
const seenRef = useRef<Set<string>>(new Set());
|
|
149
|
+
|
|
150
|
+
const adjust = useCallback((deltas: CountDelta[]) => {
|
|
151
|
+
const known = deltas.filter((delta) => latestRef.current.folders.some((folder) => folder.uid === delta.folderUid));
|
|
152
|
+
if (known.length === 0) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
changesRef.current++;
|
|
156
|
+
setCounts((previous) => {
|
|
157
|
+
const next = { ...previous };
|
|
158
|
+
for (const delta of known) {
|
|
159
|
+
const folder = latestRef.current.folders.find((f) => f.uid === delta.folderUid)!;
|
|
160
|
+
const base = next[delta.folderUid] ?? { unread: folder.unreadCount, total: folder.totalCount };
|
|
161
|
+
next[delta.folderUid] = {
|
|
162
|
+
unread: Math.max(0, base.unread + delta.unread),
|
|
163
|
+
total: Math.max(0, base.total + delta.total),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const refresh = useCallback((delayMs: number = COUNT_REFRESH_DELAY_MS) => {
|
|
171
|
+
if (stoppedRef.current) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
clearTimeout(timerRef.current);
|
|
175
|
+
if (delayMs <= 0) {
|
|
176
|
+
timerRef.current = undefined;
|
|
177
|
+
void readBackRef.current();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
timerRef.current = setTimeout(() => void readBackRef.current(), delayMs);
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
// Re-assigned on every render so it reads the latest mailboxes; `refresh` (stable) calls whichever is current.
|
|
184
|
+
readBackRef.current = async () => {
|
|
185
|
+
timerRef.current = undefined;
|
|
186
|
+
const run = ++runRef.current;
|
|
187
|
+
const startedAt = changesRef.current;
|
|
188
|
+
const results = await Promise.all(latestRef.current.mailboxes.map((mailbox) => listFolders(mailbox.uid).catch(() => undefined)));
|
|
189
|
+
if (stoppedRef.current || run !== runRef.current) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Whatever the counts' fate below, these are the mailboxes' folders as they are: a folder the sidebar does not have yet is filed now.
|
|
193
|
+
for (const list of results) {
|
|
194
|
+
if (list) {
|
|
195
|
+
latestRef.current.onFoldersListed?.(list);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (inFlightRef.current > 0 || changesRef.current !== startedAt) {
|
|
199
|
+
// A change of this page's own began or ended while that was being read, so it may predate it: read again.
|
|
200
|
+
refresh();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const fresh: Record<string, FolderCount> = {};
|
|
204
|
+
for (const list of results) {
|
|
205
|
+
for (const folder of list ?? []) {
|
|
206
|
+
fresh[folder.uid] = { unread: folder.unreadCount, total: folder.totalCount };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
setCounts((previous) => {
|
|
210
|
+
const changed = Object.entries(fresh).some(
|
|
211
|
+
([uid, count]) => previous[uid]?.unread !== count.unread || previous[uid]?.total !== count.total,
|
|
212
|
+
);
|
|
213
|
+
return changed ? { ...previous, ...fresh } : previous;
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const track = useCallback(
|
|
218
|
+
(previous: Message | null | undefined, next: Message | null | undefined): CountTracker => {
|
|
219
|
+
const deltas = countDeltas(previous, next);
|
|
220
|
+
inFlightRef.current++;
|
|
221
|
+
adjust(deltas);
|
|
222
|
+
let finished = false;
|
|
223
|
+
/** Whether this call was the one that ended it (a tracker ends once). */
|
|
224
|
+
function finish(): boolean {
|
|
225
|
+
if (finished) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
finished = true;
|
|
229
|
+
inFlightRef.current--;
|
|
230
|
+
lastDoneRef.current = Date.now();
|
|
231
|
+
changesRef.current++;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
settle() {
|
|
236
|
+
if (finish()) {
|
|
237
|
+
refresh();
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
revert() {
|
|
241
|
+
if (finish()) {
|
|
242
|
+
adjust(deltas.map((delta) => ({ folderUid: delta.folderUid, unread: -delta.unread, total: -delta.total })));
|
|
243
|
+
refresh();
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
[adjust, refresh],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const noteCreated = useCallback(
|
|
252
|
+
(message: Message) => {
|
|
253
|
+
const seen = seenRef.current;
|
|
254
|
+
if (seen.has(message.uid)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
seen.add(message.uid);
|
|
258
|
+
if (seen.size > SEEN_LIMIT) {
|
|
259
|
+
seen.delete(seen.values().next().value!);
|
|
260
|
+
}
|
|
261
|
+
adjust(countDeltas(null, message));
|
|
262
|
+
refresh();
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
[adjust, refresh],
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const applyFolderEvent = useCallback(
|
|
269
|
+
(data: unknown) => {
|
|
270
|
+
const event = data as { uid?: unknown; unreadCount?: unknown; totalCount?: unknown } | null | undefined;
|
|
271
|
+
const uid = event?.uid;
|
|
272
|
+
const folder = typeof uid === "string" ? latestRef.current.folders.find((f) => f.uid === uid) : undefined;
|
|
273
|
+
if (!folder) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (inFlightRef.current > 0 || Date.now() - lastDoneRef.current < COUNT_QUIET_MS) {
|
|
277
|
+
// Its number may be about a state older than this page's own change: let a fresh read settle it.
|
|
278
|
+
refresh();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const valid = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
282
|
+
setCounts((previous) => {
|
|
283
|
+
const current = previous[folder.uid] ?? { unread: folder.unreadCount, total: folder.totalCount };
|
|
284
|
+
const unread = valid(event!.unreadCount) ? event!.unreadCount : current.unread;
|
|
285
|
+
const total = valid(event!.totalCount) ? event!.totalCount : current.total;
|
|
286
|
+
return unread === current.unread && total === current.total && previous[folder.uid] ? previous : { ...previous, [folder.uid]: { unread, total } };
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
[refresh],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// A fresh load of the mailboxes is a fresh start: the folders come with counts of their own. (Keyed on the uids, not the array.)
|
|
293
|
+
const mailboxKey = mailboxes.map((mailbox) => mailbox.uid).join("|");
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
changesRef.current++;
|
|
296
|
+
setCounts((previous) => (Object.keys(previous).length > 0 ? {} : previous));
|
|
297
|
+
}, [mailboxKey]);
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
stoppedRef.current = false;
|
|
301
|
+
return () => {
|
|
302
|
+
stoppedRef.current = true;
|
|
303
|
+
clearTimeout(timerRef.current);
|
|
304
|
+
};
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
return { counts, track, noteCreated, applyFolderEvent, refresh };
|
|
308
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
6
|
+
import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
|
|
10
|
+
* for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
|
|
11
|
+
*/
|
|
12
|
+
export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
|
|
16
|
+
* Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
|
|
17
|
+
* to just the mail ones itself, or those other apps' folders leak into its sidebar.
|
|
18
|
+
*/
|
|
19
|
+
export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
|
|
20
|
+
|
|
21
|
+
/** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
|
|
22
|
+
export const SEND_FOLDER_TYPES = ["outbox", "sent_items"] as const;
|
|
23
|
+
|
|
24
|
+
export function isMailFolder(folder: { type: string }): boolean {
|
|
25
|
+
return MAIL_FOLDER_TYPES.has(folder.type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sortKey(type: string): number {
|
|
29
|
+
const index = FOLDER_ORDER.indexOf(type);
|
|
30
|
+
return index === -1 ? FOLDER_ORDER.length : index;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
|
|
34
|
+
export function sortFolders(folders: Folder[]): Folder[] {
|
|
35
|
+
return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What a sidebar row is: a folder that exists, or (`placeholder`) one that is expected to but has not arrived yet. */
|
|
39
|
+
export type FolderRow = { kind: "folder"; folder: Folder } | { kind: "placeholder"; type: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
|
|
43
|
+
* server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
|
|
44
|
+
* arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
|
|
45
|
+
*/
|
|
46
|
+
export function folderRows(folders: Folder[], sending: number): FolderRow[] {
|
|
47
|
+
const rows: FolderRow[] = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
|
|
48
|
+
if (sending > 0) {
|
|
49
|
+
for (const type of SEND_FOLDER_TYPES) {
|
|
50
|
+
if (!folders.some((folder) => folder.type === type)) {
|
|
51
|
+
rows.push({ kind: "placeholder", type });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rowType(row: FolderRow): string {
|
|
60
|
+
return row.kind === "folder" ? row.folder.type : row.type;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What a folder can be changed to by an event or a refetch: everything about it except the counts, which the counts overlay owns. */
|
|
64
|
+
export type FolderChange = Partial<Pick<Folder, "name" | "type" | "parentFolderUid" | "color">> & { uid: string; mailboxUid?: string };
|
|
65
|
+
|
|
66
|
+
const CHANGEABLE = ["name", "type", "parentFolderUid", "color"] as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
|
|
70
|
+
* colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
|
|
71
|
+
* nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
|
|
72
|
+
*/
|
|
73
|
+
export function upsertFolder(entries: MailboxFolders[], folder: Folder | FolderChange): MailboxFolders[] {
|
|
74
|
+
let changed = false;
|
|
75
|
+
const next = entries.map((entry) => {
|
|
76
|
+
const existing = entry.folders.find((f) => f.uid === folder.uid);
|
|
77
|
+
if (!existing) {
|
|
78
|
+
// Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
|
|
79
|
+
if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
// A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
|
|
83
|
+
// not know it existed) is not a second row: the older of the two is the one shown.
|
|
84
|
+
const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
|
|
85
|
+
if (sameType) {
|
|
86
|
+
if (!isOlder(folder, sameType)) {
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
changed = true;
|
|
90
|
+
return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
|
|
91
|
+
}
|
|
92
|
+
changed = true;
|
|
93
|
+
return { ...entry, folders: [...entry.folders, folder] };
|
|
94
|
+
}
|
|
95
|
+
const patch: Partial<Folder> = {};
|
|
96
|
+
for (const key of CHANGEABLE) {
|
|
97
|
+
if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
|
|
98
|
+
(patch as Record<string, unknown>)[key] = folder[key];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (Object.keys(patch).length === 0) {
|
|
102
|
+
return entry;
|
|
103
|
+
}
|
|
104
|
+
changed = true;
|
|
105
|
+
const updated = { ...existing, ...patch };
|
|
106
|
+
return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
|
|
107
|
+
});
|
|
108
|
+
return changed ? next : entries;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isFullFolder(folder: Folder | FolderChange): folder is Folder {
|
|
112
|
+
return typeof folder.name === "string" && typeof folder.type === "string";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether `a` was created before `b`: only when both say when. */
|
|
116
|
+
function isOlder(a: Folder, b: Folder): boolean {
|
|
117
|
+
// (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
|
|
118
|
+
const first = Date.parse(a.dateCreated);
|
|
119
|
+
const second = Date.parse(b.dateCreated);
|
|
120
|
+
return Number.isFinite(first) && Number.isFinite(second) && first < second;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Takes a deleted folder out of the tree. The same array when it was not in it. */
|
|
124
|
+
export function removeFolder(entries: MailboxFolders[], folderUid: string): MailboxFolders[] {
|
|
125
|
+
if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
|
|
126
|
+
return entries;
|
|
127
|
+
}
|
|
128
|
+
return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
|
|
133
|
+
* are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
|
|
134
|
+
* again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
|
|
135
|
+
*/
|
|
136
|
+
export function reconcileFolders(entries: MailboxFolders[], listed: Folder[]): MailboxFolders[] {
|
|
137
|
+
return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every folder uid in the tree. */
|
|
141
|
+
export function knownFolderUids(entries: MailboxFolders[]): Set<string> {
|
|
142
|
+
return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
|
|
143
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import type { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
|
|
6
|
+
import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A short-lived, in-memory copy of a folder's listing, so that switching to a folder that was shown a moment ago (or coming back
|
|
10
|
+
* to Mail from another app) shows its rows on the very frame of the click and revalidates behind them (stale-while-revalidate),
|
|
11
|
+
* instead of replacing the list with a loading state and waiting for the network. It is the client-side router's answer to
|
|
12
|
+
* "folder navigation takes seconds": the list of the folder being switched to is usually already known.
|
|
13
|
+
*
|
|
14
|
+
* Only a plain folder's listing is kept - a search's, an aggregate ("All Mailboxes") view's and a load-more page's rows are not -
|
|
15
|
+
* and what is kept is exactly what was on screen, including the rows "load more" added, the selection and the scroll position,
|
|
16
|
+
* so returning to a folder puts the reader where they left it. A snapshot only makes the first paint fast; the listing is always
|
|
17
|
+
* fetched again and folded in (`mergeFirstPage()`), so nothing shown from here is ever taken as current.
|
|
18
|
+
*/
|
|
19
|
+
export interface ListSnapshot {
|
|
20
|
+
messages: Message[];
|
|
21
|
+
conversations: ConversationSummary[];
|
|
22
|
+
hasMore: boolean;
|
|
23
|
+
/** The message that was selected (the reading pane's), if it is still in `messages` when the snapshot is shown. */
|
|
24
|
+
selectedUid: string | null;
|
|
25
|
+
/** The list's `scrollTop`, saved when the listing was left. */
|
|
26
|
+
scrollTop: number;
|
|
27
|
+
/** When it was last written. */
|
|
28
|
+
at: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** How long a snapshot may be shown after it was last written. Longer than that the folder is shown as loading, as before. */
|
|
32
|
+
export const LIST_SNAPSHOT_TTL_MS = 5 * 60_000;
|
|
33
|
+
|
|
34
|
+
/** How many folders are remembered; the least recently written is dropped first. */
|
|
35
|
+
export const LIST_SNAPSHOT_MAX = 16;
|
|
36
|
+
|
|
37
|
+
const snapshots = new Map<string, ListSnapshot>();
|
|
38
|
+
|
|
39
|
+
/** Everything that makes one listing different from another: which folder, as messages or conversations, and how it is
|
|
40
|
+
* filtered and sorted. */
|
|
41
|
+
export function listSnapshotKey(parts: {
|
|
42
|
+
mailboxUid: string | undefined;
|
|
43
|
+
folderUid: string | undefined;
|
|
44
|
+
conversations: boolean;
|
|
45
|
+
filter: string;
|
|
46
|
+
labels: string;
|
|
47
|
+
sort: string;
|
|
48
|
+
}): string {
|
|
49
|
+
return [parts.mailboxUid, parts.folderUid, parts.conversations ? "conversations" : "messages", parts.filter, parts.labels, parts.sort].join("|");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The snapshot for `key`, or `undefined` when there is none or it is older than `LIST_SNAPSHOT_TTL_MS`. */
|
|
53
|
+
export function readListSnapshot(key: string, now: number = Date.now()): ListSnapshot | undefined {
|
|
54
|
+
const snapshot = snapshots.get(key);
|
|
55
|
+
if (!snapshot) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (now - snapshot.at > LIST_SNAPSHOT_TTL_MS) {
|
|
59
|
+
snapshots.delete(key);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return snapshot;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Remembers a listing as it is on screen (keeping the scroll position saved for it, if any). */
|
|
66
|
+
export function writeListSnapshot(key: string, listing: Omit<ListSnapshot, "scrollTop" | "at">, now: number = Date.now()): void {
|
|
67
|
+
const previous = snapshots.get(key);
|
|
68
|
+
// Re-inserted so that the Map's order is least-recently-written first.
|
|
69
|
+
snapshots.delete(key);
|
|
70
|
+
snapshots.set(key, { ...listing, scrollTop: previous?.scrollTop ?? 0, at: now });
|
|
71
|
+
while (snapshots.size > LIST_SNAPSHOT_MAX) {
|
|
72
|
+
snapshots.delete(snapshots.keys().next().value as string);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Records how far `key`'s list was scrolled when it was left. A no-op for a listing that was never remembered. */
|
|
77
|
+
export function saveListScroll(key: string, scrollTop: number): void {
|
|
78
|
+
const snapshot = snapshots.get(key);
|
|
79
|
+
if (snapshot) {
|
|
80
|
+
snapshot.scrollTop = scrollTop;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Forgets every snapshot (sign-out, and tests). */
|
|
85
|
+
export function clearListSnapshots(): void {
|
|
86
|
+
snapshots.clear();
|
|
87
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { Message, setMessageRead, setMessagesRead } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
6
|
+
import type { CountTracker } from "./folderCounts.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The one way a message's read state changes in the webmail, so that every place that shows it moves together: the row in
|
|
10
|
+
* the list (and the conversation row's unread count), the folder badge, and the unread styling.
|
|
11
|
+
*
|
|
12
|
+
* It is **optimistic**: the row and the badge change the moment the user acts, the server is asked, and if it refuses the
|
|
13
|
+
* change is taken back. What each place is handed to do that is a `ReadStateSink`.
|
|
14
|
+
*/
|
|
15
|
+
export interface ReadStateSink {
|
|
16
|
+
/**
|
|
17
|
+
* Shows a message wherever the caller lists it - the message list, the conversation list's child rows, the open thread.
|
|
18
|
+
* `previous` is given with the optimistic copy (and with the revert of it) and is the copy being replaced, so a
|
|
19
|
+
* conversation row can tell that its unread count went up or down; it is absent for the server's own copy, which
|
|
20
|
+
* differs only in its `version`.
|
|
21
|
+
*/
|
|
22
|
+
patch(updated: Message, previous?: Message): void;
|
|
23
|
+
/** Applies the change to the folder badges: `MailShell`'s `trackMessageChange`. */
|
|
24
|
+
track(previous: Message, next: Message): CountTracker;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function withRead(message: Message, read: boolean): Message {
|
|
28
|
+
return { ...message, flags: { ...message.flags, read } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether `message` is not yet in the state `read` asks for. A message with no `read` flag counts as unread. */
|
|
32
|
+
function needsChange(message: Message, read: boolean): boolean {
|
|
33
|
+
return (message.flags.read === true) !== read;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Marks one message read (`read: true`) or unread. Resolves the server's updated copy, or `undefined` when nothing needed
|
|
38
|
+
* doing (it already was that way) or the server refused - in which case everything has already been put back as it was.
|
|
39
|
+
* Never rejects: like the old mark-as-read, a failure must not get in the way of reading the message.
|
|
40
|
+
*/
|
|
41
|
+
export async function setReadState(message: Message, read: boolean, sink: ReadStateSink): Promise<Message | undefined> {
|
|
42
|
+
if (!needsChange(message, read)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const optimistic = withRead(message, read);
|
|
46
|
+
sink.patch(optimistic, message);
|
|
47
|
+
const tracker = sink.track(message, optimistic);
|
|
48
|
+
try {
|
|
49
|
+
const updated = await setMessageRead(message, read);
|
|
50
|
+
sink.patch(updated);
|
|
51
|
+
tracker.settle();
|
|
52
|
+
return updated;
|
|
53
|
+
} catch {
|
|
54
|
+
sink.patch(message, optimistic);
|
|
55
|
+
tracker.revert();
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The same for a whole selection, in one request. Resolves the updated messages, in request order. Unlike `setReadState()`
|
|
62
|
+
* it rejects when the server refuses (after putting everything back), because a bulk update is not atomic - some of it may
|
|
63
|
+
* have landed - and the caller's own handling reloads the list and says so (see `bulkUpdateMessages()`).
|
|
64
|
+
*/
|
|
65
|
+
export async function setReadStateMany(messages: Message[], read: boolean, sink: ReadStateSink): Promise<Message[]> {
|
|
66
|
+
const changes = messages.filter((message) => needsChange(message, read)).map((message) => ({ message, optimistic: withRead(message, read) }));
|
|
67
|
+
const trackers = changes.map(({ message, optimistic }) => {
|
|
68
|
+
sink.patch(optimistic, message);
|
|
69
|
+
return sink.track(message, optimistic);
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const updated = await setMessagesRead(messages, read);
|
|
73
|
+
for (const message of updated) {
|
|
74
|
+
sink.patch(message);
|
|
75
|
+
}
|
|
76
|
+
trackers.forEach((tracker) => tracker.settle());
|
|
77
|
+
return updated;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
changes.forEach(({ message, optimistic }, index) => {
|
|
80
|
+
sink.patch(message, optimistic);
|
|
81
|
+
trackers[index].revert();
|
|
82
|
+
});
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|