@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,83 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* What the client-side router (`AppRouter.tsx`) offers to the tree under it - the location and a way to navigate - and the hooks that read
|
|
7
|
+
* it. Kept apart from the router itself so that the app frame (`AppShell`), which the router renders, can use `useNavigate()` without a
|
|
8
|
+
* circular import. `AppRouter.tsx` re-exports everything here, so callers keep importing from there.
|
|
9
|
+
*/
|
|
10
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
11
|
+
|
|
12
|
+
export interface RouterLocation {
|
|
13
|
+
pathname: string;
|
|
14
|
+
/** With the leading `?`, or `""`. */
|
|
15
|
+
search: string;
|
|
16
|
+
/** With the leading `#`, or `""`. */
|
|
17
|
+
hash: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NavigateOptions {
|
|
21
|
+
/** Replaces the current history entry instead of adding one (a redirect, or a state change that isn't a "place"). */
|
|
22
|
+
replace?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Goes to `href`: without a page load when it is a route of the app, else as an ordinary navigation. */
|
|
26
|
+
export type NavigateFn = (href: string, options?: NavigateOptions) => void;
|
|
27
|
+
|
|
28
|
+
export interface RouterContextValue {
|
|
29
|
+
location: RouterLocation;
|
|
30
|
+
navigate: NavigateFn;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const RouterContext = createContext<RouterContextValue | null>(null);
|
|
34
|
+
|
|
35
|
+
/** Before the browser's own location has been read (the server render and the first client render, which must match). */
|
|
36
|
+
export const UNKNOWN_LOCATION: RouterLocation = { pathname: "", search: "", hash: "" };
|
|
37
|
+
|
|
38
|
+
export function readWindowLocation(): RouterLocation {
|
|
39
|
+
const { pathname, search, hash } = window.location;
|
|
40
|
+
return { pathname, search, hash };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What `useNavigate()` does outside a router: an ordinary navigation, exactly as `window.location.href = href` always was. */
|
|
44
|
+
function navigateWithBrowser(href: string, options: NavigateOptions = {}): void {
|
|
45
|
+
if (options.replace) {
|
|
46
|
+
window.location.replace(href);
|
|
47
|
+
} else {
|
|
48
|
+
window.location.href = href;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The function to go somewhere with. `navigate("/calendar")` and `navigate("/?mailboxUid=a&folderUid=b")` change the page and
|
|
54
|
+
* the URL without a page load when the router knows the route (Mail, Calendar, Contacts, Tasks, Settings and their subpages),
|
|
55
|
+
* and load the page normally otherwise; outside the router it is always a normal navigation. Prefer a plain `<a href>` for
|
|
56
|
+
* anything the user clicks - the router intercepts it, and it stays a real link - and this for what code decides (after a
|
|
57
|
+
* save, a delete, a select). The returned function is stable across renders.
|
|
58
|
+
*/
|
|
59
|
+
export function useNavigate(): NavigateFn {
|
|
60
|
+
const router = useContext(RouterContext);
|
|
61
|
+
return router ? router.navigate : navigateWithBrowser;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Where the app is now, as `{ pathname, search, hash }`, updating as the router navigates (folder changes are `search`
|
|
66
|
+
* changes, not page changes). Empty until the browser's location has been read in an effect after the first render, so that
|
|
67
|
+
* the server render and the first client render agree - read query parameters from it in an effect, not during render.
|
|
68
|
+
*/
|
|
69
|
+
export function useLocation(): RouterLocation {
|
|
70
|
+
const router = useContext(RouterContext);
|
|
71
|
+
const [own, setOwn] = useState<RouterLocation>(UNKNOWN_LOCATION);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!router) {
|
|
74
|
+
setOwn(readWindowLocation());
|
|
75
|
+
}
|
|
76
|
+
}, [router]);
|
|
77
|
+
return router ? router.location : own;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `useLocation().search` - what the shells read `?mailboxUid=` and the like from. */
|
|
81
|
+
export function useLocationSearch(): string {
|
|
82
|
+
return useLocation().search;
|
|
83
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import type { ComponentType } from "react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One page the client-side router (`AppRouter`) can show without a page load. The table of them (`apps/www/_routes.ts`)
|
|
9
|
+
* mirrors the files under `apps/www`: what isn't in it (the admin and escrow consoles, plugin pages, anything else) is
|
|
10
|
+
* reached by a normal navigation.
|
|
11
|
+
*/
|
|
12
|
+
export interface RouteDefinition {
|
|
13
|
+
/** The route template: literal segments and `:name` parameters, the same shape as the file path (`/messages/:uid`
|
|
14
|
+
* for `messages/[uid].tsx`). The first entry that matches a URL wins, so list literal routes before parameterized
|
|
15
|
+
* ones (`/settings/filters/new` before `/settings/filters/:uid`). */
|
|
16
|
+
path: string;
|
|
17
|
+
/** The app-rail entry (or `"settings"`) highlighted while this page shows - what the page's shell passes as `active`. */
|
|
18
|
+
active: string;
|
|
19
|
+
/**
|
|
20
|
+
* Loads the page's module, which must have a `routedPage()` component as its default export. Written as
|
|
21
|
+
* `() => import("./calendar/index.js")` so the bundler makes the page its own chunk; it is called the first time the
|
|
22
|
+
* page is shown or prefetched (pointer over or focus on a link to it, or when the browser is idle).
|
|
23
|
+
*/
|
|
24
|
+
load: () => Promise<{ default: ComponentType<any> }>;
|
|
25
|
+
/** Downloaded when the browser is idle after the first page has loaded - the app rail's own pages, which are the
|
|
26
|
+
* likeliest next click. */
|
|
27
|
+
idlePrefetch?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RouteMatch {
|
|
31
|
+
route: RouteDefinition;
|
|
32
|
+
/** The values of the template's `:name` segments, decoded. */
|
|
33
|
+
params: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Splits a path into segments, ignoring a trailing slash: `/` and `""` have none. */
|
|
37
|
+
function segmentsOf(path: string): string[] {
|
|
38
|
+
return path.split("/").filter((segment) => segment !== "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Matches `pathname` against one route template - `undefined` when it doesn't, or a parameter isn't valid percent-encoding. */
|
|
42
|
+
function matchTemplate(template: string, pathname: string): Record<string, string> | undefined {
|
|
43
|
+
const expected = segmentsOf(template);
|
|
44
|
+
const actual = segmentsOf(pathname);
|
|
45
|
+
if (expected.length !== actual.length) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const params: Record<string, string> = {};
|
|
49
|
+
for (let i = 0; i < expected.length; i++) {
|
|
50
|
+
if (expected[i].startsWith(":")) {
|
|
51
|
+
try {
|
|
52
|
+
params[expected[i].slice(1)] = decodeURIComponent(actual[i]);
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
} else if (expected[i] !== actual[i]) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return params;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The first route in `routes` whose template matches `pathname`, with its parameters. */
|
|
64
|
+
export function matchRoute(routes: readonly RouteDefinition[], pathname: string): RouteMatch | undefined {
|
|
65
|
+
for (const route of routes) {
|
|
66
|
+
const params = matchTemplate(route.path, pathname);
|
|
67
|
+
if (params) {
|
|
68
|
+
return { route, params };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect } from "react";
|
|
6
|
+
import type { IconType } from "react-icons";
|
|
7
|
+
import {
|
|
8
|
+
HiOutlineCheckCircle,
|
|
9
|
+
HiOutlineEnvelope,
|
|
10
|
+
HiOutlineExclamationCircle,
|
|
11
|
+
HiOutlineExclamationTriangle,
|
|
12
|
+
HiOutlineInformationCircle,
|
|
13
|
+
HiOutlineXMark,
|
|
14
|
+
} from "react-icons/hi2";
|
|
15
|
+
import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
16
|
+
import { NotificationAction, NotificationKind, NotificationView, dismiss, setAllPaused, setPaused } from "./store.js";
|
|
17
|
+
import { useNotifications } from "./useNotifications.js";
|
|
18
|
+
import { useSystemErrorNotifications } from "./systemErrors.js";
|
|
19
|
+
|
|
20
|
+
const KIND_STYLE: Record<NotificationKind, { border: string; icon: string; Icon: IconType; label: string }> = {
|
|
21
|
+
mail: { border: "border-l-primary", icon: "text-primary-dark", Icon: HiOutlineEnvelope, label: "New message" },
|
|
22
|
+
info: { border: "border-l-primary", icon: "text-primary-dark", Icon: HiOutlineInformationCircle, label: "Information" },
|
|
23
|
+
success: { border: "border-l-success", icon: "text-success", Icon: HiOutlineCheckCircle, label: "Success" },
|
|
24
|
+
warning: { border: "border-l-warning", icon: "text-warning", Icon: HiOutlineExclamationTriangle, label: "Warning" },
|
|
25
|
+
error: { border: "border-l-danger", icon: "text-danger", Icon: HiOutlineExclamationCircle, label: "Error" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ACTION_CLASS =
|
|
29
|
+
"rounded-sm px-2.5 py-1 text-xs font-semibold text-primary-dark hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary";
|
|
30
|
+
|
|
31
|
+
/** Stops every clock while the tab is out of view: nobody could have read what is on screen, and it is still there when they come back. */
|
|
32
|
+
function usePauseWhileHidden(): void {
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const update = () => setAllPaused(document.visibilityState === "hidden");
|
|
35
|
+
update();
|
|
36
|
+
document.addEventListener("visibilitychange", update);
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener("visibilitychange", update);
|
|
39
|
+
setAllPaused(false);
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ActionButton({ action, id }: { action: NotificationAction; id: string }) {
|
|
45
|
+
function run() {
|
|
46
|
+
action.onClick?.();
|
|
47
|
+
if (!action.keepOpen) {
|
|
48
|
+
dismiss(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return action.href ? (
|
|
52
|
+
<a href={action.href} onClick={run} className={ACTION_CLASS}>
|
|
53
|
+
{action.label}
|
|
54
|
+
</a>
|
|
55
|
+
) : (
|
|
56
|
+
<button type="button" onClick={run} className={ACTION_CLASS}>
|
|
57
|
+
{action.label}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The expandable, copyable technical lines behind a notification. */
|
|
63
|
+
function Details({ lines }: { lines: string[] }) {
|
|
64
|
+
return (
|
|
65
|
+
<details className="mt-1.5">
|
|
66
|
+
<summary className="cursor-pointer text-xs font-semibold text-text-muted">Technical details</summary>
|
|
67
|
+
<ul
|
|
68
|
+
aria-label="Technical details"
|
|
69
|
+
className="mt-1.5 max-h-40 overflow-auto rounded-sm border border-border bg-surface-alt p-2 font-mono text-xs text-text select-text"
|
|
70
|
+
>
|
|
71
|
+
{lines.map((line, index) => (
|
|
72
|
+
<li key={index} className="whitespace-pre-wrap break-words">
|
|
73
|
+
{line}
|
|
74
|
+
</li>
|
|
75
|
+
))}
|
|
76
|
+
</ul>
|
|
77
|
+
<div className="mt-1.5">
|
|
78
|
+
<CopyButton value={lines.join("\n")} label="Copy technical details" />
|
|
79
|
+
</div>
|
|
80
|
+
</details>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One pop-up. Its clock (kept by the store) stops while it is hovered or holds the keyboard focus; Escape dismisses it. */
|
|
85
|
+
function NotificationItem({ notification }: { notification: NotificationView }) {
|
|
86
|
+
const { id, kind, title, subtitle, message, preview, hint, details, actions, href, count } = notification;
|
|
87
|
+
const style = KIND_STYLE[kind];
|
|
88
|
+
const body = (
|
|
89
|
+
<>
|
|
90
|
+
{kind === "mail" && <span className="block text-xs font-semibold uppercase tracking-wide text-primary-dark">{style.label}</span>}
|
|
91
|
+
<span className="flex min-w-0 items-baseline gap-1.5 text-sm">
|
|
92
|
+
<span className={["truncate font-bold", kind === "error" ? "text-danger" : ""].join(" ")}>{title}</span>
|
|
93
|
+
{count > 1 && (
|
|
94
|
+
<span className="shrink-0 rounded-pill bg-surface-alt px-1.5 text-xs font-semibold text-text-muted">
|
|
95
|
+
<span aria-hidden="true">×{count}</span>
|
|
96
|
+
<span className="sr-only"> ({count} times)</span>
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
{subtitle && <span className="truncate text-xs text-text-muted">{subtitle}</span>}
|
|
100
|
+
</span>
|
|
101
|
+
{message && <span className={["mt-0.5 block text-sm [overflow-wrap:anywhere]", kind === "mail" ? "truncate font-semibold" : ""].join(" ")}>{message}</span>}
|
|
102
|
+
{preview && <span className="mt-0.5 block text-xs text-text-muted [overflow-wrap:anywhere]">{preview}</span>}
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
data-testid="notification"
|
|
108
|
+
role={kind === "error" ? "alert" : "status"}
|
|
109
|
+
data-kind={kind}
|
|
110
|
+
data-notification-id={id}
|
|
111
|
+
onMouseEnter={() => setPaused(id, true)}
|
|
112
|
+
onMouseLeave={() => setPaused(id, false)}
|
|
113
|
+
onFocus={() => setPaused(id, true)}
|
|
114
|
+
onBlur={(event) => {
|
|
115
|
+
// Focus moving between this pop-up's own controls is not leaving it.
|
|
116
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
117
|
+
setPaused(id, false);
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
onKeyDown={(event) => {
|
|
121
|
+
if (event.key === "Escape") {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
dismiss(id);
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
className={`rr-toast-in pointer-events-auto relative rounded-md border border-border border-l-4 ${style.border} bg-surface text-text shadow-lg`}
|
|
127
|
+
>
|
|
128
|
+
<div className="flex items-start gap-2 p-3 pr-10">
|
|
129
|
+
{kind !== "mail" && <style.Icon size={18} aria-hidden="true" className={`mt-0.5 shrink-0 ${style.icon}`} />}
|
|
130
|
+
<div className="min-w-0 flex-1">
|
|
131
|
+
{href ? (
|
|
132
|
+
<a
|
|
133
|
+
href={href}
|
|
134
|
+
onClick={() => dismiss(id)}
|
|
135
|
+
className="-m-1 block rounded-md p-1 hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary"
|
|
136
|
+
>
|
|
137
|
+
{body}
|
|
138
|
+
</a>
|
|
139
|
+
) : (
|
|
140
|
+
body
|
|
141
|
+
)}
|
|
142
|
+
{hint && <p className="mt-1.5 text-xs text-text-muted">{hint}</p>}
|
|
143
|
+
{details.length > 0 && <Details lines={details} />}
|
|
144
|
+
{actions.length > 0 && (
|
|
145
|
+
<div className="-ml-2.5 mt-1.5 flex flex-wrap items-center gap-1">
|
|
146
|
+
{actions.map((action, index) => (
|
|
147
|
+
<ActionButton key={index} action={action} id={id} />
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={() => dismiss(id)}
|
|
156
|
+
aria-label={`Dismiss notification: ${title}`}
|
|
157
|
+
className="absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text focus-visible:outline-2 focus-visible:outline-primary"
|
|
158
|
+
>
|
|
159
|
+
<HiOutlineXMark size={16} aria-hidden="true" />
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The pop-up stack, and the one place the app's notifications (see `store.ts`) are drawn. Mounted once, in the persistent app frame, so
|
|
167
|
+
* it survives page changes and covers every app.
|
|
168
|
+
*
|
|
169
|
+
* **Where it sits.** Top right, *below* the header row: the stack is anchored to a zero-height `sticky` line placed right after the
|
|
170
|
+
* frame's header, so it starts under the header (and so under the account menu button, whatever height a branded header has) and, as
|
|
171
|
+
* the page scrolls, sticks just below the header (`--rr-header-h`, published by the title bar / `FrameBrandingHeader` - see `headerOffset.ts`;
|
|
172
|
+
* never less than 2.5 rem, which clears a full-screen compose sheet's own title bar on a phone).
|
|
173
|
+
* On a desktop the compose windows are anchored at the bottom right, the far end of the screen from this; it never covers the account menu,
|
|
174
|
+
* the header's actions or a compose window's title bar and Send button unless the window is stretched over most of the screen height.
|
|
175
|
+
* It sits above the compose sheet (z-index 60 against 50), so a pop-up is never hidden behind it. Its height is capped and scrolls.
|
|
176
|
+
*
|
|
177
|
+
* **Live regions.** Two always-mounted containers with `aria-live` (a live region has to exist before its content changes to be announced):
|
|
178
|
+
* errors in an assertive one, everything else in a polite one, and each pop-up carries the matching role - `alert` for an error, `status`
|
|
179
|
+
* for the rest - so it is announced accordingly, and the empty containers add no `alert`/`status` of their own to the page.
|
|
180
|
+
* `prefers-reduced-motion` switches the entrance animation off (`.rr-toast-in`).
|
|
181
|
+
*/
|
|
182
|
+
export default function NotificationCenter() {
|
|
183
|
+
const { visible, queued } = useNotifications();
|
|
184
|
+
usePauseWhileHidden();
|
|
185
|
+
useSystemErrorNotifications();
|
|
186
|
+
const errors = visible.filter((notification) => notification.kind === "error");
|
|
187
|
+
const others = visible.filter((notification) => notification.kind !== "error");
|
|
188
|
+
return (
|
|
189
|
+
<div data-testid="notification-anchor" className="sticky top-[max(var(--rr-header-h,0px),2.5rem)] z-[60] h-0 w-full">
|
|
190
|
+
<div
|
|
191
|
+
data-testid="notification-stack"
|
|
192
|
+
// Never taller than the room above the open compose windows (`ComposeProvider` publishes their top edge as `--rr-compose-top`), so
|
|
193
|
+
// a stack of pop-ups scrolls inside its own box rather than covering a compose window's title bar and buttons.
|
|
194
|
+
// (The padding is room for the pop-ups' shadow, which the scroll box would otherwise cut off in a visible edge.)
|
|
195
|
+
className="pointer-events-none absolute right-0 -top-1 flex max-h-[max(8rem,calc(var(--rr-compose-top,100vh)-max(var(--rr-header-h,0px),2.5rem)-2rem))] w-[25rem] max-w-[calc(100vw-0.5rem)] flex-col overflow-y-auto p-4"
|
|
196
|
+
>
|
|
197
|
+
<div data-testid="notification-errors" aria-live="assertive" className="flex flex-col gap-2 [&:not(:empty)]:mb-2">
|
|
198
|
+
{errors.map((notification) => (
|
|
199
|
+
<NotificationItem key={notification.id} notification={notification} />
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
<div data-testid="notification-others" aria-live="polite" className="flex flex-col gap-2">
|
|
203
|
+
{others.map((notification) => (
|
|
204
|
+
<NotificationItem key={notification.id} notification={notification} />
|
|
205
|
+
))}
|
|
206
|
+
{queued > 0 && (
|
|
207
|
+
<p className="rr-solid pointer-events-auto self-end rounded-pill bg-surface px-2.5 py-0.5 text-xs text-text-muted shadow-md">
|
|
208
|
+
{queued} more waiting
|
|
209
|
+
</p>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useRef } from "react";
|
|
6
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
7
|
+
import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
8
|
+
import { HistoryEntry, NotificationKind } from "./store.js";
|
|
9
|
+
import { useNotifications } from "./useNotifications.js";
|
|
10
|
+
|
|
11
|
+
const KIND_LABEL: Record<NotificationKind, string> = {
|
|
12
|
+
mail: "New message",
|
|
13
|
+
info: "Information",
|
|
14
|
+
success: "Done",
|
|
15
|
+
warning: "Warning",
|
|
16
|
+
error: "Error",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function HistoryItem({ entry, unseen }: { entry: HistoryEntry; unseen: boolean }) {
|
|
20
|
+
return (
|
|
21
|
+
<li className={["border-b border-border py-2.5 last:border-b-0", unseen ? "-mx-2 rounded-sm bg-danger-bg/60 px-2" : ""].join(" ")}>
|
|
22
|
+
<div className="flex items-baseline justify-between gap-3 text-xs text-text-muted">
|
|
23
|
+
<span className={["font-semibold uppercase tracking-wide", entry.kind === "error" ? "text-danger" : ""].join(" ")}>{KIND_LABEL[entry.kind]}</span>
|
|
24
|
+
<time dateTime={new Date(entry.at).toISOString()}>{new Date(entry.at).toLocaleTimeString()}</time>
|
|
25
|
+
</div>
|
|
26
|
+
<p className="mt-0.5 text-sm font-semibold [overflow-wrap:anywhere]">
|
|
27
|
+
{entry.title}
|
|
28
|
+
{entry.count > 1 && <span className="ml-1.5 text-xs font-medium text-text-muted">×{entry.count}</span>}
|
|
29
|
+
</p>
|
|
30
|
+
{entry.message && <p className="text-sm text-text-muted [overflow-wrap:anywhere]">{entry.message}</p>}
|
|
31
|
+
{entry.details.length > 0 && (
|
|
32
|
+
<details className="mt-1">
|
|
33
|
+
<summary className="cursor-pointer text-xs font-semibold text-text-muted">Technical details</summary>
|
|
34
|
+
<ul aria-label="Technical details" className="mt-1.5 max-h-40 overflow-auto rounded-sm border border-border bg-surface-alt p-2 font-mono text-xs select-text">
|
|
35
|
+
{entry.details.map((line, index) => (
|
|
36
|
+
<li key={index} className="whitespace-pre-wrap break-words">
|
|
37
|
+
{line}
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
<div className="mt-1.5">
|
|
42
|
+
<CopyButton value={entry.details.join("\n")} label="Copy technical details" />
|
|
43
|
+
</div>
|
|
44
|
+
</details>
|
|
45
|
+
)}
|
|
46
|
+
</li>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface NotificationHistoryDialogProps {
|
|
51
|
+
open: boolean;
|
|
52
|
+
onClose: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* "Recent notifications": the last few pop-ups, newest first, so nothing is lost when one expires. Opening it counts as having seen
|
|
57
|
+
* them (the unseen-error count on the account menu goes), but the errors that were unseen when it opened stay highlighted until it closes.
|
|
58
|
+
*/
|
|
59
|
+
export default function NotificationHistoryDialog({ open, onClose }: NotificationHistoryDialogProps) {
|
|
60
|
+
const { history, markHistorySeen, clearHistory } = useNotifications();
|
|
61
|
+
const unseenAtOpenRef = useRef<Set<string>>(new Set());
|
|
62
|
+
if (!open) {
|
|
63
|
+
unseenAtOpenRef.current = new Set(history.filter((entry) => entry.unseen && entry.kind === "error").map((entry) => entry.id));
|
|
64
|
+
}
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (open) {
|
|
67
|
+
markHistorySeen();
|
|
68
|
+
}
|
|
69
|
+
}, [open, markHistorySeen]);
|
|
70
|
+
return (
|
|
71
|
+
<Modal open={open} onClose={onClose} title="Recent notifications">
|
|
72
|
+
{history.length === 0 ? (
|
|
73
|
+
<p className="text-sm text-text-muted">Nothing yet. Errors, sent messages and other pop-ups from this session are listed here.</p>
|
|
74
|
+
) : (
|
|
75
|
+
<>
|
|
76
|
+
<ul aria-label="Recent notifications">
|
|
77
|
+
{history.map((entry) => (
|
|
78
|
+
<HistoryItem key={entry.id} entry={entry} unseen={unseenAtOpenRef.current.has(entry.id)} />
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
<div className="mt-4">
|
|
82
|
+
<button type="button" onClick={clearHistory} className="rounded-sm px-2.5 py-1 text-xs font-semibold text-primary-dark hover:bg-surface-alt">
|
|
83
|
+
Clear list
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
</Modal>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
6
|
+
import { describeSendFailure } from "@rapidmx/react-shared/mail/sendFailure.js";
|
|
7
|
+
import { NotificationAction, notify } from "./store.js";
|
|
8
|
+
|
|
9
|
+
/** Whether `err` is a request that was cancelled (`AbortError`) - by name, since a `DOMException` is not always an `Error` (jsdom's is not). */
|
|
10
|
+
export function isAbortError(err: unknown): boolean {
|
|
11
|
+
return typeof err === "object" && err !== null && (err as { name?: unknown }).name === "AbortError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Where "Sign in" goes: auth-server's base URL, set once by the app frame. Without one it reloads the page, which redirects when signed out. */
|
|
15
|
+
let signInBaseUrl: string | undefined;
|
|
16
|
+
|
|
17
|
+
export function setSignInUrl(authServerUrl: string | undefined): void {
|
|
18
|
+
signInBaseUrl = authServerUrl;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The "Your session expired" pop-up: one however many requests noticed, with a "Sign in" action. Returns its id. */
|
|
22
|
+
export function notifySessionExpired(): string {
|
|
23
|
+
return notify({
|
|
24
|
+
kind: "error",
|
|
25
|
+
title: "Your session expired",
|
|
26
|
+
message: "Sign in again to keep working. Anything not yet saved in this tab is still here until you leave it.",
|
|
27
|
+
dedupeKey: "session-expired",
|
|
28
|
+
actions: [
|
|
29
|
+
{
|
|
30
|
+
label: "Sign in",
|
|
31
|
+
onClick: () => {
|
|
32
|
+
if (signInBaseUrl) {
|
|
33
|
+
window.location.href = `${signInBaseUrl}/auth/signin?return_to=${encodeURIComponent(window.location.href)}`;
|
|
34
|
+
} else {
|
|
35
|
+
window.location.reload();
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface NotifyApiErrorOptions {
|
|
44
|
+
/** Extra actions (a "Retry"). */
|
|
45
|
+
actions?: NotificationAction[];
|
|
46
|
+
/** Replaces the default key (`api:<context>:<status>`): errors with one key are one pop-up with a count. */
|
|
47
|
+
dedupeKey?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether `err` is a request that never got an answer (offline, DNS, a dropped connection) rather than an error response. */
|
|
51
|
+
function isNetworkFailure(err: unknown): boolean {
|
|
52
|
+
return err instanceof TypeError || (err instanceof Error && /network|failed to fetch|load failed/i.test(err.message));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Turns the failure of something the user did into an `error` pop-up and returns its id - or, for a `401`, into "Your session expired"
|
|
57
|
+
* with a Sign in action, and for a request that was cancelled into nothing (`""`). `context` says what was being attempted, as the
|
|
58
|
+
* pop-up's title ("Couldn't archive this message"); the message is the server's own, and its technical facts (`describeSendFailure()`'s
|
|
59
|
+
* lines, the status and code) are the expandable details. The same failing thing repeating is one pop-up with a count.
|
|
60
|
+
*/
|
|
61
|
+
export function notifyApiError(err: unknown, context: string, options: NotifyApiErrorOptions = {}): string {
|
|
62
|
+
if (isAbortError(err)) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
if (err instanceof ApiRequestError) {
|
|
66
|
+
if (err.status === 401) {
|
|
67
|
+
return notifySessionExpired();
|
|
68
|
+
}
|
|
69
|
+
const failure = describeSendFailure(err, context);
|
|
70
|
+
const facts = [`Status: ${err.status}${err.code ? ` (${err.code})` : ""}`, ...failure.lines];
|
|
71
|
+
return notify({
|
|
72
|
+
kind: "error",
|
|
73
|
+
title: context,
|
|
74
|
+
message: failure.message,
|
|
75
|
+
details: facts,
|
|
76
|
+
actions: options.actions,
|
|
77
|
+
dedupeKey: options.dedupeKey ?? `api:${context}:${err.status}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (isNetworkFailure(err)) {
|
|
81
|
+
return notify({
|
|
82
|
+
kind: "error",
|
|
83
|
+
title: context,
|
|
84
|
+
message: "The server couldn't be reached. Check your connection and try again.",
|
|
85
|
+
details: [(err as Error).message],
|
|
86
|
+
actions: options.actions,
|
|
87
|
+
dedupeKey: options.dedupeKey ?? `api:${context}:network`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return notify({
|
|
91
|
+
kind: "error",
|
|
92
|
+
title: context,
|
|
93
|
+
message: "Something unexpected went wrong.",
|
|
94
|
+
details: err instanceof Error ? [`${err.name}: ${err.message}`] : [String(err)],
|
|
95
|
+
actions: options.actions,
|
|
96
|
+
dedupeKey: options.dedupeKey ?? `api:${context}:unexpected`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useCallback, useRef } from "react";
|
|
6
|
+
|
|
7
|
+
/** The CSS variable (on `<html>`) that holds the height, in px, of the frame's sticky header - its title bar or the branding header. */
|
|
8
|
+
export const HEADER_HEIGHT_VAR = "--rr-header-h";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A ref callback for the frame's sticky `<header>`: publishes its height as `--rr-header-h` (and keeps it current as the header resizes -
|
|
12
|
+
* a branded header can be any height and wraps on a phone), so the pop-up stack (`NotificationCenter`) can stick just below it instead of
|
|
13
|
+
* sliding under it or over the account menu. When the header goes (a page without one) the variable is set to 0 rather than left stale.
|
|
14
|
+
*/
|
|
15
|
+
export function useHeaderHeightRef(): (element: HTMLElement | null) => void {
|
|
16
|
+
const stop = useRef<(() => void) | undefined>(undefined);
|
|
17
|
+
return useCallback((element: HTMLElement | null) => {
|
|
18
|
+
stop.current?.();
|
|
19
|
+
stop.current = undefined;
|
|
20
|
+
const root = document.documentElement;
|
|
21
|
+
if (!element) {
|
|
22
|
+
root.style.setProperty(HEADER_HEIGHT_VAR, "0px");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const publish = () => root.style.setProperty(HEADER_HEIGHT_VAR, `${Math.round(element.getBoundingClientRect().height)}px`);
|
|
26
|
+
publish();
|
|
27
|
+
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(publish);
|
|
28
|
+
observer?.observe(element);
|
|
29
|
+
window.addEventListener("resize", publish);
|
|
30
|
+
stop.current = () => {
|
|
31
|
+
observer?.disconnect();
|
|
32
|
+
window.removeEventListener("resize", publish);
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
import { getPushClient, PushStatus } from "@rapidmx/react-shared/mail/pushClient.js";
|
|
7
|
+
import { dismiss, notify } from "./store.js";
|
|
8
|
+
|
|
9
|
+
/** How long the live connection has to be down before anyone is told: a reconnect within a few seconds is not worth a word. */
|
|
10
|
+
export const PUSH_OFFLINE_NOTICE_MS = 10_000;
|
|
11
|
+
|
|
12
|
+
export const PUSH_NOTICE_ID = "push-connection";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A subtle info pop-up when the live connection (new mail, folder counts, send results) has been down for more than `PUSH_OFFLINE_NOTICE_MS`,
|
|
16
|
+
* which goes by itself the moment it is back. Shown once per outage - dismissing it does not bring it back until the connection has
|
|
17
|
+
* reconnected and dropped again. The page keeps working meanwhile (it polls), which is what the message says.
|
|
18
|
+
*/
|
|
19
|
+
export function usePushConnectionNotice(enabled: boolean): void {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const client = getPushClient();
|
|
25
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
26
|
+
let shown = false;
|
|
27
|
+
const off = client.onStatus((status: PushStatus) => {
|
|
28
|
+
if (status === "reconnecting") {
|
|
29
|
+
if (timer === undefined && !shown) {
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
shown = true;
|
|
32
|
+
notify({
|
|
33
|
+
id: PUSH_NOTICE_ID,
|
|
34
|
+
kind: "info",
|
|
35
|
+
title: "Live updates are paused",
|
|
36
|
+
message: "The connection is being restored. The page keeps refreshing on its own, so new mail may take a little longer to appear.",
|
|
37
|
+
sticky: true,
|
|
38
|
+
history: false,
|
|
39
|
+
});
|
|
40
|
+
}, PUSH_OFFLINE_NOTICE_MS);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
timer = undefined;
|
|
45
|
+
if (status === "open" && shown) {
|
|
46
|
+
shown = false;
|
|
47
|
+
dismiss(PUSH_NOTICE_ID);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return () => {
|
|
52
|
+
off();
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
};
|
|
55
|
+
}, [enabled]);
|
|
56
|
+
}
|