@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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shortcuts that are mounted right now. Components add and remove their claims as they mount and unmount, so a page switch changes
|
|
3
|
+
* what a key does with nothing to re-wire; the dispatcher (`dispatchKeyEvent()`) looks handlers up here at the time of the key press,
|
|
4
|
+
* and the help dialog lists what is here.
|
|
5
|
+
*/
|
|
6
|
+
export class ShortcutRegistry {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.registrations = [];
|
|
9
|
+
this.counter = 0;
|
|
10
|
+
this.listeners = new Set();
|
|
11
|
+
/** The current claims, oldest first. A new array whenever they change (so it can be a `useSyncExternalStore()` snapshot). */
|
|
12
|
+
this.getSnapshot = () => this.registrations;
|
|
13
|
+
this.subscribe = (listener) => {
|
|
14
|
+
this.listeners.add(listener);
|
|
15
|
+
return () => {
|
|
16
|
+
this.listeners.delete(listener);
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Adds a claim; returns what removes it. */
|
|
21
|
+
add(claim) {
|
|
22
|
+
const registration = { ...claim, id: ++this.counter, order: this.counter };
|
|
23
|
+
this.registrations = [...this.registrations, registration];
|
|
24
|
+
this.publish();
|
|
25
|
+
return () => {
|
|
26
|
+
this.registrations = this.registrations.filter((existing) => existing.id !== registration.id);
|
|
27
|
+
this.publish();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
publish() {
|
|
31
|
+
this.listeners.forEach((listener) => listener());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Whether the event target takes typed text: a text field, a select, a textarea, or anything contenteditable (the rich-text editor). */
|
|
2
|
+
export declare function isTextEntry(target: EventTarget | null): boolean;
|
|
3
|
+
/** Whether the target is inside a widget that reads bare keys itself (an open menu, a listbox, a tab list, ...). */
|
|
4
|
+
export declare function isInWidget(target: EventTarget | null): boolean;
|
|
5
|
+
/** Whether Enter/Space on the target already activates it (a button, a link, a menu item). */
|
|
6
|
+
export declare function isActivatable(target: EventTarget | null): boolean;
|
|
7
|
+
/** Whether the target is the trigger of a popup that is open right now (or the combobox that owns an open list). */
|
|
8
|
+
export declare function isPopupOpenAt(target: EventTarget | null): boolean;
|
|
9
|
+
/** Whether a modal dialog (`aria-modal="true"`: react-shared's `Modal` and `Drawer`, the unlock prompts, ...) is on screen. */
|
|
10
|
+
export declare function isModalOpen(doc: Document): boolean;
|
|
11
|
+
/** Whether the target is inside a popover that is a dialog of its own (the emoji and GIF pickers, the send-later picker): Escape is theirs, not a view's. A region that declares a shortcut scope (a compose window) is not one. */
|
|
12
|
+
export declare function isInPopoverDialog(target: EventTarget | null): boolean;
|
|
13
|
+
/** The `data-shortcut-scope` of the region the target is in (a compose window), if any. */
|
|
14
|
+
export declare function regionScopeOf(target: EventTarget | null): string | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** `<input>` types that take no typed text, so a letter pressed on one is a shortcut, not input. (Radios and ranges use the arrows.) */
|
|
6
|
+
const NON_TEXT_INPUT_TYPES = new Set(["button", "checkbox", "submit", "reset", "image", "file", "color"]);
|
|
7
|
+
/** Roles whose members have keys of their own (arrows, Enter, Escape, type-ahead): a bare key pressed inside one belongs to the widget. */
|
|
8
|
+
const WIDGET_SELECTOR = [
|
|
9
|
+
"menu",
|
|
10
|
+
"menubar",
|
|
11
|
+
"listbox",
|
|
12
|
+
"tablist",
|
|
13
|
+
"tree",
|
|
14
|
+
"grid",
|
|
15
|
+
"radiogroup",
|
|
16
|
+
"slider",
|
|
17
|
+
"spinbutton",
|
|
18
|
+
"combobox",
|
|
19
|
+
]
|
|
20
|
+
.map((role) => `[role="${role}"]`)
|
|
21
|
+
.join(",");
|
|
22
|
+
/** Elements Enter (and Space) already does something on. */
|
|
23
|
+
const ACTIVATABLE_SELECTOR = 'a[href], button, summary, [role="button"], [role="link"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="tab"], [role="option"]';
|
|
24
|
+
/** An open popup's trigger: Escape closes the popup, so it is not a shortcut then. */
|
|
25
|
+
const OPEN_POPUP_TRIGGER = '[aria-haspopup][aria-expanded="true"], [role="combobox"][aria-expanded="true"]';
|
|
26
|
+
function elementOf(target) {
|
|
27
|
+
return target instanceof Element ? target : null;
|
|
28
|
+
}
|
|
29
|
+
/** Whether the event target takes typed text: a text field, a select, a textarea, or anything contenteditable (the rich-text editor). */
|
|
30
|
+
export function isTextEntry(target) {
|
|
31
|
+
const element = elementOf(target);
|
|
32
|
+
if (!element) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const tag = element.tagName;
|
|
36
|
+
if (tag === "TEXTAREA" || tag === "SELECT") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (tag === "INPUT") {
|
|
40
|
+
return !NON_TEXT_INPUT_TYPES.has(element.type);
|
|
41
|
+
}
|
|
42
|
+
return !!element.closest('[contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="searchbox"]');
|
|
43
|
+
}
|
|
44
|
+
/** Whether the target is inside a widget that reads bare keys itself (an open menu, a listbox, a tab list, ...). */
|
|
45
|
+
export function isInWidget(target) {
|
|
46
|
+
return !!elementOf(target)?.closest(WIDGET_SELECTOR);
|
|
47
|
+
}
|
|
48
|
+
/** Whether Enter/Space on the target already activates it (a button, a link, a menu item). */
|
|
49
|
+
export function isActivatable(target) {
|
|
50
|
+
return !!elementOf(target)?.closest(ACTIVATABLE_SELECTOR);
|
|
51
|
+
}
|
|
52
|
+
/** Whether the target is the trigger of a popup that is open right now (or the combobox that owns an open list). */
|
|
53
|
+
export function isPopupOpenAt(target) {
|
|
54
|
+
return !!elementOf(target)?.closest(OPEN_POPUP_TRIGGER);
|
|
55
|
+
}
|
|
56
|
+
/** Whether a modal dialog (`aria-modal="true"`: react-shared's `Modal` and `Drawer`, the unlock prompts, ...) is on screen. */
|
|
57
|
+
export function isModalOpen(doc) {
|
|
58
|
+
return doc.querySelector('[aria-modal="true"]') !== null;
|
|
59
|
+
}
|
|
60
|
+
/** Whether the target is inside a popover that is a dialog of its own (the emoji and GIF pickers, the send-later picker): Escape is theirs, not a view's. A region that declares a shortcut scope (a compose window) is not one. */
|
|
61
|
+
export function isInPopoverDialog(target) {
|
|
62
|
+
return !!elementOf(target)?.closest('[role="dialog"]:not([data-shortcut-scope])');
|
|
63
|
+
}
|
|
64
|
+
/** The `data-shortcut-scope` of the region the target is in (a compose window), if any. */
|
|
65
|
+
export function regionScopeOf(target) {
|
|
66
|
+
return elementOf(target)?.closest("[data-shortcut-scope]")?.getAttribute("data-shortcut-scope") ?? undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
import type { ShortcutDef, ShortcutScope } from "./keymap.js";
|
|
3
|
+
import type { ShortcutHandler } from "./registry.js";
|
|
4
|
+
export interface UseShortcutOptions {
|
|
5
|
+
/** Overrides the scope the shortcut is defined with - required for a bare spec string, which has none. Default `global`. */
|
|
6
|
+
scope?: ShortcutScope;
|
|
7
|
+
/** Registers only while true: a view offers a shortcut only when it can really do the thing. Default true. */
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
/** Restricts it to events whose target is inside this element (a compose window). */
|
|
10
|
+
container?: RefObject<Element | null>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Claims a shortcut for as long as the calling component is mounted (and `enabled`). `shortcut` is an entry of `SHORTCUTS` - which is what
|
|
14
|
+
* the help dialog and tooltips describe - or a bare spec such as `"alt+n"` for a one-off. The handler is read at the time of the key press,
|
|
15
|
+
* so it always sees the component's current state without the registration being redone; return `false` from it to decline the key.
|
|
16
|
+
*
|
|
17
|
+
* Does nothing outside a `ShortcutProvider`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useShortcut(shortcut: ShortcutDef | string, handler: ShortcutHandler, options?: UseShortcutOptions): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { parseSpec } from "./parse.js";
|
|
7
|
+
import { useShortcutRegistry } from "./ShortcutProvider.js";
|
|
8
|
+
/**
|
|
9
|
+
* Claims a shortcut for as long as the calling component is mounted (and `enabled`). `shortcut` is an entry of `SHORTCUTS` - which is what
|
|
10
|
+
* the help dialog and tooltips describe - or a bare spec such as `"alt+n"` for a one-off. The handler is read at the time of the key press,
|
|
11
|
+
* so it always sees the component's current state without the registration being redone; return `false` from it to decline the key.
|
|
12
|
+
*
|
|
13
|
+
* Does nothing outside a `ShortcutProvider`.
|
|
14
|
+
*/
|
|
15
|
+
export function useShortcut(shortcut, handler, options = {}) {
|
|
16
|
+
const { scope, enabled = true, container } = options;
|
|
17
|
+
const registry = useShortcutRegistry();
|
|
18
|
+
const handlerRef = useRef(handler);
|
|
19
|
+
handlerRef.current = handler;
|
|
20
|
+
const def = useMemo(() => (typeof shortcut === "string" ? { id: `custom:${shortcut}`, keys: [shortcut], label: shortcut, scope: "global" } : shortcut), [shortcut]);
|
|
21
|
+
const resolvedScope = scope ?? def.scope;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!registry || !enabled) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
return registry.add({
|
|
27
|
+
shortcut: def,
|
|
28
|
+
scope: resolvedScope,
|
|
29
|
+
run: (event) => handlerRef.current(event),
|
|
30
|
+
container,
|
|
31
|
+
keys: def.keys.map(parseSpec),
|
|
32
|
+
electronKeys: (def.electronKeys ?? []).map(parseSpec),
|
|
33
|
+
});
|
|
34
|
+
}, [registry, enabled, def, resolvedScope, container]);
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ShortcutDef } from "./keymap.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a control that a keyboard shortcut also triggers gets: a tooltip naming the key - `Reply (Ctrl+R)`, in this platform's own key names -
|
|
4
|
+
* and `aria-keyshortcuts` for assistive technology. The accessible name is left alone (the control keeps its own label), so a tooltip suffix never
|
|
5
|
+
* changes what a screen reader or a test finds it by. With `active` false - the shortcut isn't registered right now - it is just the plain label.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useShortcutProps(label: string, shortcut: ShortcutDef, active?: boolean): {
|
|
8
|
+
title: string;
|
|
9
|
+
"aria-keyshortcuts"?: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ariaKeyShortcuts, withHint } from "./format.js";
|
|
6
|
+
import { useKeyEnvironment } from "./ShortcutProvider.js";
|
|
7
|
+
/**
|
|
8
|
+
* What a control that a keyboard shortcut also triggers gets: a tooltip naming the key - `Reply (Ctrl+R)`, in this platform's own key names -
|
|
9
|
+
* and `aria-keyshortcuts` for assistive technology. The accessible name is left alone (the control keeps its own label), so a tooltip suffix never
|
|
10
|
+
* changes what a screen reader or a test finds it by. With `active` false - the shortcut isn't registered right now - it is just the plain label.
|
|
11
|
+
*/
|
|
12
|
+
export function useShortcutProps(label, shortcut, active = true) {
|
|
13
|
+
const env = useKeyEnvironment();
|
|
14
|
+
return active ? { title: withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) } : { title: label };
|
|
15
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Folder, FolderType, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
/**
|
|
3
|
+
* The folder badges' numbers, kept right without a reload.
|
|
4
|
+
*
|
|
5
|
+
* A folder's `unreadCount`/`totalCount` come from the server (`GET /mail/folders`, the server-published `Folder` update
|
|
6
|
+
* event, and the poll), but waiting for a round trip to see a badge move after reading a message feels broken, so a change
|
|
7
|
+
* made here is applied to the badge at once - **optimistically** - and then *reconciled*: every so often, and after every
|
|
8
|
+
* change, the real counts are read back and replace whatever this page worked out. So the page can be wrong for a moment,
|
|
9
|
+
* never for good.
|
|
10
|
+
*
|
|
11
|
+
* What the server sends (`@rapidmx/restapi`): `GET /mail/folders` returns each folder with a derived `unreadCount`/`totalCount`
|
|
12
|
+
* (unread messages, all messages), and after any change to a folder's messages it publishes
|
|
13
|
+
* `{ type: /^Folder/, action: "update", data: { uid, mailboxUid, unreadCount, totalCount } }` on the folder's push channel *and* its
|
|
14
|
+
* mailbox's, so each is heard twice - the values are absolute and applying one again changes nothing. Nothing here fails without
|
|
15
|
+
* that event, or for a write that publishes none (ActiveSync/MAPI, erasure jobs): the read-back after each change and the poll do
|
|
16
|
+
* the same job a little later, and overlapping writes to one folder are unordered, so they always have the last word.
|
|
17
|
+
*/
|
|
18
|
+
export interface FolderCount {
|
|
19
|
+
unread: number;
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
/** A change to one folder's counts. */
|
|
23
|
+
export interface CountDelta {
|
|
24
|
+
folderUid: string;
|
|
25
|
+
unread: number;
|
|
26
|
+
total: number;
|
|
27
|
+
}
|
|
28
|
+
/** What one message change - a new message (`previous` absent), a deletion (`next` absent), a read/unread flip, a move - does to the folders' counts. */
|
|
29
|
+
export declare function countDeltas(previous: Message | null | undefined, next: Message | null | undefined): CountDelta[];
|
|
30
|
+
/** 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. */
|
|
31
|
+
export declare function countOfFolder(folder: Folder, counts: Record<string, FolderCount>): FolderCount;
|
|
32
|
+
/** What a folder's badge shows: how many unread, or (Drafts and Outbox, Outlook-style) how many messages are in it. */
|
|
33
|
+
export interface FolderBadge {
|
|
34
|
+
kind: "unread" | "total";
|
|
35
|
+
value: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The badge a folder gets, if any. Inbox, Archive and the user's own folders show their unread count when it is above
|
|
39
|
+
* zero; Drafts and Outbox show how many messages they hold (an unread count means nothing there); Sent Items, Deleted
|
|
40
|
+
* Items and Junk Email show nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function badgeFor(type: FolderType, count: FolderCount): FolderBadge | undefined;
|
|
43
|
+
/** The badge's text for assistive technology: "3 unread", "2 messages". */
|
|
44
|
+
export declare function badgeLabel(badge: FolderBadge): string;
|
|
45
|
+
/** The unread messages in every mailbox's Inbox - what the tab title counts. */
|
|
46
|
+
export declare function inboxUnreadTotal(mailboxFolders: {
|
|
47
|
+
folders: Folder[];
|
|
48
|
+
}[], counts: Record<string, FolderCount>): number;
|
|
49
|
+
/** How long after the last change of this page's own has finished a server-published count is trusted again: one published
|
|
50
|
+
* about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
|
|
51
|
+
export declare const COUNT_QUIET_MS = 1500;
|
|
52
|
+
/** How long after a change the counts are read back from the server. */
|
|
53
|
+
export declare const COUNT_REFRESH_DELAY_MS = 600;
|
|
54
|
+
/** An optimistic change in flight: call exactly one of these once the server has answered. */
|
|
55
|
+
export interface CountTracker {
|
|
56
|
+
/** The server accepted the change: keep what was applied, and read the real counts back. */
|
|
57
|
+
settle(): void;
|
|
58
|
+
/** The server refused it (or it failed): put the counts back as they were, and read the real ones back. */
|
|
59
|
+
revert(): void;
|
|
60
|
+
}
|
|
61
|
+
export interface FolderCounts {
|
|
62
|
+
/** The counts this page has worked out or been told since load, by folder uid - see `countOfFolder()`. */
|
|
63
|
+
counts: Record<string, FolderCount>;
|
|
64
|
+
/** Applies the effect of changing `previous` into `next` now, before the server has answered - see `CountTracker`. */
|
|
65
|
+
track(previous: Message | null | undefined, next: Message | null | undefined): CountTracker;
|
|
66
|
+
/** A new message arrived over the push connection: counts it (once per message) until the real counts are read back.
|
|
67
|
+
* `false` when this one was already counted - a duplicate event. */
|
|
68
|
+
noteCreated(message: Message): boolean;
|
|
69
|
+
/** The server's `Folder` update event's `data`: the folder's real counts - when this page has nothing of its own in flight. */
|
|
70
|
+
applyFolderEvent(data: unknown): void;
|
|
71
|
+
/** Reads every mailbox's real counts back and shows them, after `delayMs` (at once for 0). A later call replaces one still waiting. */
|
|
72
|
+
refresh(delayMs?: number): void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Holds the folder counts overlay for `MailShell`. `folders` is every folder the sidebar lists (the overlay starts from what
|
|
76
|
+
* each was loaded with); `mailboxes` is whose folders to read back. The overlay starts again whenever `mailboxes` does.
|
|
77
|
+
*/
|
|
78
|
+
export declare function useFolderCounts(mailboxes: Mailbox[], folders: Folder[], onFoldersListed?: (folders: Folder[]) => void): FolderCounts;
|
|
@@ -0,0 +1,218 @@
|
|
|
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 { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
/** What one message change - a new message (`previous` absent), a deletion (`next` absent), a read/unread flip, a move - does to the folders' counts. */
|
|
8
|
+
export function countDeltas(previous, next) {
|
|
9
|
+
const byFolder = new Map();
|
|
10
|
+
function add(message, sign) {
|
|
11
|
+
const entry = byFolder.get(message.folderUid) ?? { folderUid: message.folderUid, unread: 0, total: 0 };
|
|
12
|
+
entry.total += sign;
|
|
13
|
+
entry.unread += message.flags.read === true ? 0 : sign;
|
|
14
|
+
byFolder.set(message.folderUid, entry);
|
|
15
|
+
}
|
|
16
|
+
if (previous) {
|
|
17
|
+
add(previous, -1);
|
|
18
|
+
}
|
|
19
|
+
if (next) {
|
|
20
|
+
add(next, 1);
|
|
21
|
+
}
|
|
22
|
+
return [...byFolder.values()].filter((delta) => delta.unread !== 0 || delta.total !== 0);
|
|
23
|
+
}
|
|
24
|
+
/** 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. */
|
|
25
|
+
export function countOfFolder(folder, counts) {
|
|
26
|
+
return counts[folder.uid] ?? { unread: folder.unreadCount, total: folder.totalCount };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The badge a folder gets, if any. Inbox, Archive and the user's own folders show their unread count when it is above
|
|
30
|
+
* zero; Drafts and Outbox show how many messages they hold (an unread count means nothing there); Sent Items, Deleted
|
|
31
|
+
* Items and Junk Email show nothing.
|
|
32
|
+
*/
|
|
33
|
+
export function badgeFor(type, count) {
|
|
34
|
+
switch (type) {
|
|
35
|
+
case "sent_items":
|
|
36
|
+
case "deleted_items":
|
|
37
|
+
case "junk":
|
|
38
|
+
return undefined;
|
|
39
|
+
case "drafts":
|
|
40
|
+
case "outbox":
|
|
41
|
+
return count.total > 0 ? { kind: "total", value: count.total } : undefined;
|
|
42
|
+
default:
|
|
43
|
+
return count.unread > 0 ? { kind: "unread", value: count.unread } : undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The badge's text for assistive technology: "3 unread", "2 messages". */
|
|
47
|
+
export function badgeLabel(badge) {
|
|
48
|
+
return badge.kind === "unread" ? `${badge.value} unread` : `${badge.value} ${badge.value === 1 ? "message" : "messages"}`;
|
|
49
|
+
}
|
|
50
|
+
/** The unread messages in every mailbox's Inbox - what the tab title counts. */
|
|
51
|
+
export function inboxUnreadTotal(mailboxFolders, counts) {
|
|
52
|
+
return mailboxFolders.reduce((total, entry) => total + entry.folders.filter((folder) => folder.type === "inbox").reduce((sum, folder) => sum + countOfFolder(folder, counts).unread, 0), 0);
|
|
53
|
+
}
|
|
54
|
+
/** How long after the last change of this page's own has finished a server-published count is trusted again: one published
|
|
55
|
+
* about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
|
|
56
|
+
export const COUNT_QUIET_MS = 1500;
|
|
57
|
+
/** How long after a change the counts are read back from the server. */
|
|
58
|
+
export const COUNT_REFRESH_DELAY_MS = 600;
|
|
59
|
+
/** How many message uids the "already counted this new message" memory holds. */
|
|
60
|
+
const SEEN_LIMIT = 500;
|
|
61
|
+
/**
|
|
62
|
+
* Holds the folder counts overlay for `MailShell`. `folders` is every folder the sidebar lists (the overlay starts from what
|
|
63
|
+
* each was loaded with); `mailboxes` is whose folders to read back. The overlay starts again whenever `mailboxes` does.
|
|
64
|
+
*/
|
|
65
|
+
export function useFolderCounts(mailboxes, folders, onFoldersListed) {
|
|
66
|
+
const [counts, setCounts] = useState({});
|
|
67
|
+
const latestRef = useRef({ mailboxes, folders, onFoldersListed });
|
|
68
|
+
latestRef.current = { mailboxes, folders, onFoldersListed };
|
|
69
|
+
// Bumped by every change of the overlay this page makes, so a read that started before one can tell it is out of date.
|
|
70
|
+
const changesRef = useRef(0);
|
|
71
|
+
const inFlightRef = useRef(0);
|
|
72
|
+
const lastDoneRef = useRef(0);
|
|
73
|
+
const runRef = useRef(0);
|
|
74
|
+
// Assigned during every render, before anything can call it.
|
|
75
|
+
const readBackRef = useRef(undefined);
|
|
76
|
+
const timerRef = useRef(undefined);
|
|
77
|
+
const stoppedRef = useRef(false);
|
|
78
|
+
const seenRef = useRef(new Set());
|
|
79
|
+
const adjust = useCallback((deltas) => {
|
|
80
|
+
const known = deltas.filter((delta) => latestRef.current.folders.some((folder) => folder.uid === delta.folderUid));
|
|
81
|
+
if (known.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
changesRef.current++;
|
|
85
|
+
setCounts((previous) => {
|
|
86
|
+
const next = { ...previous };
|
|
87
|
+
for (const delta of known) {
|
|
88
|
+
const folder = latestRef.current.folders.find((f) => f.uid === delta.folderUid);
|
|
89
|
+
const base = next[delta.folderUid] ?? { unread: folder.unreadCount, total: folder.totalCount };
|
|
90
|
+
next[delta.folderUid] = {
|
|
91
|
+
unread: Math.max(0, base.unread + delta.unread),
|
|
92
|
+
total: Math.max(0, base.total + delta.total),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
}, []);
|
|
98
|
+
const refresh = useCallback((delayMs = COUNT_REFRESH_DELAY_MS) => {
|
|
99
|
+
if (stoppedRef.current) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
clearTimeout(timerRef.current);
|
|
103
|
+
if (delayMs <= 0) {
|
|
104
|
+
timerRef.current = undefined;
|
|
105
|
+
void readBackRef.current();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
timerRef.current = setTimeout(() => void readBackRef.current(), delayMs);
|
|
109
|
+
}, []);
|
|
110
|
+
// Re-assigned on every render so it reads the latest mailboxes; `refresh` (stable) calls whichever is current.
|
|
111
|
+
readBackRef.current = async () => {
|
|
112
|
+
timerRef.current = undefined;
|
|
113
|
+
const run = ++runRef.current;
|
|
114
|
+
const startedAt = changesRef.current;
|
|
115
|
+
const results = await Promise.all(latestRef.current.mailboxes.map((mailbox) => listFolders(mailbox.uid).catch(() => undefined)));
|
|
116
|
+
if (stoppedRef.current || run !== runRef.current) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Whatever the counts' fate below, these are the mailboxes' folders as they are: a folder the sidebar does not have yet is filed now.
|
|
120
|
+
for (const list of results) {
|
|
121
|
+
if (list) {
|
|
122
|
+
latestRef.current.onFoldersListed?.(list);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (inFlightRef.current > 0 || changesRef.current !== startedAt) {
|
|
126
|
+
// A change of this page's own began or ended while that was being read, so it may predate it: read again.
|
|
127
|
+
refresh();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const fresh = {};
|
|
131
|
+
for (const list of results) {
|
|
132
|
+
for (const folder of list ?? []) {
|
|
133
|
+
fresh[folder.uid] = { unread: folder.unreadCount, total: folder.totalCount };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
setCounts((previous) => {
|
|
137
|
+
const changed = Object.entries(fresh).some(([uid, count]) => previous[uid]?.unread !== count.unread || previous[uid]?.total !== count.total);
|
|
138
|
+
return changed ? { ...previous, ...fresh } : previous;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const track = useCallback((previous, next) => {
|
|
142
|
+
const deltas = countDeltas(previous, next);
|
|
143
|
+
inFlightRef.current++;
|
|
144
|
+
adjust(deltas);
|
|
145
|
+
let finished = false;
|
|
146
|
+
/** Whether this call was the one that ended it (a tracker ends once). */
|
|
147
|
+
function finish() {
|
|
148
|
+
if (finished) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
finished = true;
|
|
152
|
+
inFlightRef.current--;
|
|
153
|
+
lastDoneRef.current = Date.now();
|
|
154
|
+
changesRef.current++;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
settle() {
|
|
159
|
+
if (finish()) {
|
|
160
|
+
refresh();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
revert() {
|
|
164
|
+
if (finish()) {
|
|
165
|
+
adjust(deltas.map((delta) => ({ folderUid: delta.folderUid, unread: -delta.unread, total: -delta.total })));
|
|
166
|
+
refresh();
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}, [adjust, refresh]);
|
|
171
|
+
const noteCreated = useCallback((message) => {
|
|
172
|
+
const seen = seenRef.current;
|
|
173
|
+
if (seen.has(message.uid)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
seen.add(message.uid);
|
|
177
|
+
if (seen.size > SEEN_LIMIT) {
|
|
178
|
+
seen.delete(seen.values().next().value);
|
|
179
|
+
}
|
|
180
|
+
adjust(countDeltas(null, message));
|
|
181
|
+
refresh();
|
|
182
|
+
return true;
|
|
183
|
+
}, [adjust, refresh]);
|
|
184
|
+
const applyFolderEvent = useCallback((data) => {
|
|
185
|
+
const event = data;
|
|
186
|
+
const uid = event?.uid;
|
|
187
|
+
const folder = typeof uid === "string" ? latestRef.current.folders.find((f) => f.uid === uid) : undefined;
|
|
188
|
+
if (!folder) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (inFlightRef.current > 0 || Date.now() - lastDoneRef.current < COUNT_QUIET_MS) {
|
|
192
|
+
// Its number may be about a state older than this page's own change: let a fresh read settle it.
|
|
193
|
+
refresh();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const valid = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
197
|
+
setCounts((previous) => {
|
|
198
|
+
const current = previous[folder.uid] ?? { unread: folder.unreadCount, total: folder.totalCount };
|
|
199
|
+
const unread = valid(event.unreadCount) ? event.unreadCount : current.unread;
|
|
200
|
+
const total = valid(event.totalCount) ? event.totalCount : current.total;
|
|
201
|
+
return unread === current.unread && total === current.total && previous[folder.uid] ? previous : { ...previous, [folder.uid]: { unread, total } };
|
|
202
|
+
});
|
|
203
|
+
}, [refresh]);
|
|
204
|
+
// 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.)
|
|
205
|
+
const mailboxKey = mailboxes.map((mailbox) => mailbox.uid).join("|");
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
changesRef.current++;
|
|
208
|
+
setCounts((previous) => (Object.keys(previous).length > 0 ? {} : previous));
|
|
209
|
+
}, [mailboxKey]);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
stoppedRef.current = false;
|
|
212
|
+
return () => {
|
|
213
|
+
stoppedRef.current = true;
|
|
214
|
+
clearTimeout(timerRef.current);
|
|
215
|
+
};
|
|
216
|
+
}, []);
|
|
217
|
+
return { counts, track, noteCreated, applyFolderEvent, refresh };
|
|
218
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
|
|
3
|
+
/**
|
|
4
|
+
* The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
|
|
5
|
+
* for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FOLDER_ORDER: string[];
|
|
8
|
+
/**
|
|
9
|
+
* A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
|
|
10
|
+
* Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
|
|
11
|
+
* to just the mail ones itself, or those other apps' folders leak into its sidebar.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MAIL_FOLDER_TYPES: Set<string>;
|
|
14
|
+
/** 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. */
|
|
15
|
+
export declare const SEND_FOLDER_TYPES: readonly ["outbox", "sent_items"];
|
|
16
|
+
export declare function isMailFolder(folder: {
|
|
17
|
+
type: string;
|
|
18
|
+
}): boolean;
|
|
19
|
+
/** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
|
|
20
|
+
export declare function sortFolders(folders: Folder[]): Folder[];
|
|
21
|
+
/** What a sidebar row is: a folder that exists, or (`placeholder`) one that is expected to but has not arrived yet. */
|
|
22
|
+
export type FolderRow = {
|
|
23
|
+
kind: "folder";
|
|
24
|
+
folder: Folder;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "placeholder";
|
|
27
|
+
type: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
|
|
31
|
+
* 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
|
|
32
|
+
* arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
|
|
33
|
+
*/
|
|
34
|
+
export declare function folderRows(folders: Folder[], sending: number): FolderRow[];
|
|
35
|
+
/** What a folder can be changed to by an event or a refetch: everything about it except the counts, which the counts overlay owns. */
|
|
36
|
+
export type FolderChange = Partial<Pick<Folder, "name" | "type" | "parentFolderUid" | "color">> & {
|
|
37
|
+
uid: string;
|
|
38
|
+
mailboxUid?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* 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
|
|
42
|
+
* 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
|
|
43
|
+
* nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
|
|
44
|
+
*/
|
|
45
|
+
export declare function upsertFolder(entries: MailboxFolders[], folder: Folder | FolderChange): MailboxFolders[];
|
|
46
|
+
/** Takes a deleted folder out of the tree. The same array when it was not in it. */
|
|
47
|
+
export declare function removeFolder(entries: MailboxFolders[], folderUid: string): MailboxFolders[];
|
|
48
|
+
/**
|
|
49
|
+
* 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
|
|
50
|
+
* 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
|
|
51
|
+
* again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
|
|
52
|
+
*/
|
|
53
|
+
export declare function reconcileFolders(entries: MailboxFolders[], listed: Folder[]): MailboxFolders[];
|
|
54
|
+
/** Every folder uid in the tree. */
|
|
55
|
+
export declare function knownFolderUids(entries: MailboxFolders[]): Set<string>;
|