gloomberb 0.11.0 → 0.11.2
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 +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Box, Text, useUiHost } from "../../../ui";
|
|
2
|
+
import { Box, Text, useUiCapabilities, useUiHost } from "../../../ui";
|
|
3
3
|
import {
|
|
4
4
|
ChoiceDialog,
|
|
5
5
|
Tabs,
|
|
@@ -32,7 +32,9 @@ import { SeriesEditorDialog } from "./editor";
|
|
|
32
32
|
import { chartComposerSemanticMetadata } from "./semantic";
|
|
33
33
|
import {
|
|
34
34
|
canToggleChartSeries,
|
|
35
|
+
CHART_INTERACTION_VIEWPORT_SETTING_KEY,
|
|
35
36
|
CHART_SPEC_SETTING_KEY,
|
|
37
|
+
parseChartInteractionViewport,
|
|
36
38
|
parseChartSpecOr,
|
|
37
39
|
projectVisibleChartSeries,
|
|
38
40
|
toggleChartSeries,
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
type BuiltinStudySelection,
|
|
51
53
|
type PairStudySelection,
|
|
52
54
|
} from "./presets";
|
|
55
|
+
import type { ChartInteractionViewport } from "./chart-spec";
|
|
53
56
|
import {
|
|
54
57
|
CHART_FORMULA_OPTIONS,
|
|
55
58
|
CHART_RANGES as RANGES,
|
|
@@ -59,6 +62,9 @@ import {
|
|
|
59
62
|
import { resolveChartComposerShortcut } from "./shortcuts";
|
|
60
63
|
import { ChartSeriesQuickAdd } from "./quick-add";
|
|
61
64
|
import { useLiveStreamingSetting } from "../shared/live-streaming";
|
|
65
|
+
import { usePublicShare } from "../shared/public-share";
|
|
66
|
+
import { buildChartShareData } from "../../../shares/chart-snapshot";
|
|
67
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
62
68
|
|
|
63
69
|
const RANGE_TABS = RANGES.map((range, index) => ({ label: `${index + 1}:${range}`, value: range }));
|
|
64
70
|
const AUTO_VIEWPORT_DEBOUNCE_MS = 350;
|
|
@@ -68,6 +74,28 @@ interface RuntimeChartViewport {
|
|
|
68
74
|
end: Date;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
interface RuntimeChartViewportState {
|
|
78
|
+
key: string;
|
|
79
|
+
adaptiveViewport: RuntimeChartViewport | null;
|
|
80
|
+
requestViewport: RuntimeChartViewport;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runtimeViewportFromSetting(
|
|
84
|
+
setting: ChartInteractionViewport | null,
|
|
85
|
+
authoredViewportKey: string,
|
|
86
|
+
): RuntimeChartViewportState | null {
|
|
87
|
+
if (!setting || setting.authoredViewportKey !== authoredViewportKey) return null;
|
|
88
|
+
const requestViewport = {
|
|
89
|
+
start: new Date(setting.start),
|
|
90
|
+
end: new Date(setting.end),
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
key: authoredViewportKey,
|
|
94
|
+
adaptiveViewport: setting.adaptive ? requestViewport : null,
|
|
95
|
+
requestViewport,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
function footerAnchorPoint(event?: PaneFooterPressEvent): { x: number; y: number } | undefined {
|
|
72
100
|
const x = event?.pixelX;
|
|
73
101
|
const y = event?.pixelY;
|
|
@@ -84,6 +112,7 @@ interface ChartComposerSurfaceProps {
|
|
|
84
112
|
height: number;
|
|
85
113
|
footerId: string;
|
|
86
114
|
onCapture?: (capturing: boolean) => void;
|
|
115
|
+
liveWhenUnfocused?: boolean;
|
|
87
116
|
}
|
|
88
117
|
|
|
89
118
|
const QUICK_ADD_CAPTURE = "quick-add";
|
|
@@ -103,10 +132,12 @@ function ChartComposerSurface({
|
|
|
103
132
|
height,
|
|
104
133
|
footerId,
|
|
105
134
|
onCapture,
|
|
135
|
+
liveWhenUnfocused = true,
|
|
106
136
|
}: ChartComposerSurfaceProps) {
|
|
107
137
|
const dialog = useDialog();
|
|
108
138
|
const dispatch = useAppDispatch();
|
|
109
139
|
const isDesktopWeb = useUiHost().kind === "desktop-web";
|
|
140
|
+
const { publicSharing } = useUiCapabilities();
|
|
110
141
|
const paneId = usePaneInstanceId();
|
|
111
142
|
const liveStreaming = useLiveStreamingSetting();
|
|
112
143
|
const dialogOpen = useDialogState((state) => state.isOpen);
|
|
@@ -131,11 +162,17 @@ function ChartComposerSurface({
|
|
|
131
162
|
? [entry.id, entry.source.kind, entry.source.seriesId]
|
|
132
163
|
: [entry.id, entry.source.kind, chartSeriesSourceKey(entry.source)]),
|
|
133
164
|
}), [spec.series, spec.viewport.dateWindow, spec.viewport.maxPoints, spec.viewport.range, spec.viewport.resolution]);
|
|
134
|
-
const [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
const [storedInteractionViewport, setStoredInteractionViewport] = usePaneSettingValue<unknown>(
|
|
166
|
+
CHART_INTERACTION_VIEWPORT_SETTING_KEY,
|
|
167
|
+
null,
|
|
168
|
+
);
|
|
169
|
+
const persistedInteractionViewport = useMemo(
|
|
170
|
+
() => parseChartInteractionViewport(storedInteractionViewport),
|
|
171
|
+
[storedInteractionViewport],
|
|
172
|
+
);
|
|
173
|
+
const [runtimeViewportState, setRuntimeViewportState] = useState<RuntimeChartViewportState | null>(() => (
|
|
174
|
+
runtimeViewportFromSetting(persistedInteractionViewport, authoredViewportKey)
|
|
175
|
+
));
|
|
139
176
|
const runtimeViewportTimerRef = useRef<ReturnType<typeof globalThis.setTimeout> | null>(null);
|
|
140
177
|
const adaptiveViewportRef = useRef<RuntimeChartViewport | null>(null);
|
|
141
178
|
const requestViewportRef = useRef<RuntimeChartViewport | null>(null);
|
|
@@ -149,7 +186,7 @@ function ChartComposerSurface({
|
|
|
149
186
|
: null,
|
|
150
187
|
requestViewport: activeRuntimeViewport?.requestViewport,
|
|
151
188
|
targetPointCount,
|
|
152
|
-
liveStreaming,
|
|
189
|
+
liveStreaming: liveStreaming && (liveWhenUnfocused || focused),
|
|
153
190
|
});
|
|
154
191
|
const availableResolutions = useMemo<ChartResolution[]>(() => {
|
|
155
192
|
if (!resolution.resolutionSupport) {
|
|
@@ -220,6 +257,11 @@ function ChartComposerSurface({
|
|
|
220
257
|
),
|
|
221
258
|
[resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
|
|
222
259
|
);
|
|
260
|
+
const shareData = useMemo(() => buildChartShareData(plottedSeries), [plottedSeries]);
|
|
261
|
+
const createPublicShare = usePublicShare();
|
|
262
|
+
const shareChart = useCallback(() => {
|
|
263
|
+
if (shareData) void createPublicShare({ kind: "chart", data: shareData });
|
|
264
|
+
}, [createPublicShare, shareData]);
|
|
223
265
|
const [interactionCaptured, setInteractionCapturedState] = useState(false);
|
|
224
266
|
// Typing in quick-add must not freeze the plot: it only takes the keyboard.
|
|
225
267
|
const [modalCaptured, setModalCaptured] = useState(false);
|
|
@@ -261,16 +303,21 @@ function ChartComposerSurface({
|
|
|
261
303
|
clearTimeout(runtimeViewportTimerRef.current);
|
|
262
304
|
runtimeViewportTimerRef.current = null;
|
|
263
305
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
306
|
+
const restored = runtimeViewportFromSetting(
|
|
307
|
+
persistedInteractionViewport,
|
|
308
|
+
authoredViewportKey,
|
|
309
|
+
);
|
|
310
|
+
adaptiveViewportRef.current = restored?.adaptiveViewport ?? null;
|
|
311
|
+
requestViewportRef.current = restored?.requestViewport ?? null;
|
|
312
|
+
setRuntimeViewportState((current) => current?.key === authoredViewportKey ? current : restored);
|
|
313
|
+
if (persistedInteractionViewport && !restored) setStoredInteractionViewport(null);
|
|
267
314
|
return () => {
|
|
268
315
|
if (runtimeViewportTimerRef.current !== null) {
|
|
269
316
|
clearTimeout(runtimeViewportTimerRef.current);
|
|
270
317
|
runtimeViewportTimerRef.current = null;
|
|
271
318
|
}
|
|
272
319
|
};
|
|
273
|
-
}, [authoredViewportKey]);
|
|
320
|
+
}, [authoredViewportKey, persistedInteractionViewport, setStoredInteractionViewport]);
|
|
274
321
|
const handleChartViewportChange = useCallback((
|
|
275
322
|
next: { start: Date; end: Date } | null,
|
|
276
323
|
interaction: "pan" | "reset" | "sync" | "zoom",
|
|
@@ -284,6 +331,7 @@ function ChartComposerSurface({
|
|
|
284
331
|
adaptiveViewportRef.current = null;
|
|
285
332
|
requestViewportRef.current = null;
|
|
286
333
|
setRuntimeViewportState(null);
|
|
334
|
+
setStoredInteractionViewport(null);
|
|
287
335
|
return;
|
|
288
336
|
}
|
|
289
337
|
const start = next.start.getTime();
|
|
@@ -306,8 +354,14 @@ function ChartComposerSurface({
|
|
|
306
354
|
adaptiveViewport,
|
|
307
355
|
requestViewport,
|
|
308
356
|
});
|
|
357
|
+
setStoredInteractionViewport({
|
|
358
|
+
authoredViewportKey,
|
|
359
|
+
start: requestViewport.start.toISOString(),
|
|
360
|
+
end: requestViewport.end.toISOString(),
|
|
361
|
+
adaptive: adaptiveViewport !== null,
|
|
362
|
+
} satisfies ChartInteractionViewport);
|
|
309
363
|
}, AUTO_VIEWPORT_DEBOUNCE_MS);
|
|
310
|
-
}, [authoredViewportKey, spec.viewport.resolution]);
|
|
364
|
+
}, [authoredViewportKey, setStoredInteractionViewport, spec.viewport.resolution]);
|
|
311
365
|
|
|
312
366
|
useRemoteUiNode({
|
|
313
367
|
role: "chart-data",
|
|
@@ -444,6 +498,12 @@ function ChartComposerSurface({
|
|
|
444
498
|
|
|
445
499
|
useShortcut((event) => {
|
|
446
500
|
if (interactionCaptureRef.current || dialogOpen) return;
|
|
501
|
+
if (publicSharing && shareData && isPlainKey(event, "y")) {
|
|
502
|
+
event.preventDefault();
|
|
503
|
+
event.stopPropagation();
|
|
504
|
+
shareChart();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
447
507
|
const shortcut = resolveChartComposerShortcut(event, RANGES.length);
|
|
448
508
|
if (!shortcut) return;
|
|
449
509
|
event.preventDefault();
|
|
@@ -479,6 +539,9 @@ function ChartComposerSurface({
|
|
|
479
539
|
{ id: "formulas", key: "f", label: "ormulas", onPress: openFormulas, disabled: formulasDisabled },
|
|
480
540
|
{ id: "resolution", key: "t", label: "imeframe", onPress: footerResolution },
|
|
481
541
|
{ id: "range", key: "1-8", label: "range", onPress: footerRange },
|
|
542
|
+
...(publicSharing
|
|
543
|
+
? [{ id: "share", key: "y", label: " share", onPress: shareChart, disabled: !shareData }]
|
|
544
|
+
: []),
|
|
482
545
|
],
|
|
483
546
|
}), [
|
|
484
547
|
footerRange,
|
|
@@ -489,6 +552,9 @@ function ChartComposerSurface({
|
|
|
489
552
|
indicatorsDisabled,
|
|
490
553
|
openFormulas,
|
|
491
554
|
openIndicators,
|
|
555
|
+
publicSharing,
|
|
556
|
+
shareChart,
|
|
557
|
+
shareData,
|
|
492
558
|
resolution.errors,
|
|
493
559
|
resolution.loading,
|
|
494
560
|
resolution.warnings,
|
|
@@ -635,6 +701,20 @@ export function ChartComposerPane({ paneId, focused, width, height }: PaneProps)
|
|
|
635
701
|
);
|
|
636
702
|
}
|
|
637
703
|
|
|
704
|
+
function firstChartSecuritySymbol(spec: ChartSpec): string | null {
|
|
705
|
+
for (const entry of spec.series) {
|
|
706
|
+
if (entry.source.kind === "security") return entry.source.instrument.symbol;
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function specHasSecuritySymbol(spec: ChartSpec, symbol: string | null | undefined): boolean {
|
|
712
|
+
if (!symbol) return false;
|
|
713
|
+
return spec.series.some((entry) => (
|
|
714
|
+
entry.source.kind === "security" && entry.source.instrument.symbol === symbol
|
|
715
|
+
));
|
|
716
|
+
}
|
|
717
|
+
|
|
638
718
|
export function ChartComposerResearchTab({ focused, width, height, onCapture }: TickerResearchTabProps) {
|
|
639
719
|
const { symbol } = usePaneTicker();
|
|
640
720
|
const fallback = useMemo(() => symbol ? buildPriceChartPreset(symbol) : buildEmptyChartPreset(), [symbol]);
|
|
@@ -643,11 +723,18 @@ export function ChartComposerResearchTab({ focused, width, height, onCapture }:
|
|
|
643
723
|
const previousSymbolRef = useRef(symbol);
|
|
644
724
|
|
|
645
725
|
useEffect(() => {
|
|
726
|
+
if (!symbol) return;
|
|
646
727
|
const previousSymbol = previousSymbolRef.current;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
728
|
+
const fromSymbol = specHasSecuritySymbol(spec, previousSymbol)
|
|
729
|
+
? previousSymbol
|
|
730
|
+
: firstChartSecuritySymbol(spec) ?? previousSymbol;
|
|
731
|
+
if (!fromSymbol || fromSymbol === symbol) {
|
|
732
|
+
previousSymbolRef.current = symbol;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const rebound = rebindChartSecuritySymbol(spec, fromSymbol, symbol);
|
|
650
736
|
if (rebound !== spec) setStoredSpec(rebound);
|
|
737
|
+
previousSymbolRef.current = symbol;
|
|
651
738
|
}, [setStoredSpec, spec, symbol]);
|
|
652
739
|
|
|
653
740
|
return (
|
|
@@ -659,6 +746,7 @@ export function ChartComposerResearchTab({ focused, width, height, onCapture }:
|
|
|
659
746
|
height={height}
|
|
660
747
|
footerId="chart-composer:research"
|
|
661
748
|
onCapture={onCapture}
|
|
749
|
+
liveWhenUnfocused={false}
|
|
662
750
|
/>
|
|
663
751
|
);
|
|
664
752
|
}
|
|
@@ -240,10 +240,11 @@ export function ChatContent({
|
|
|
240
240
|
const {
|
|
241
241
|
cancelProfilePopoverClose,
|
|
242
242
|
closeProfilePopover,
|
|
243
|
+
ownProfileConfigured,
|
|
243
244
|
profilePopoverUser,
|
|
244
245
|
scheduleProfilePopoverClose,
|
|
245
246
|
showProfilePopover,
|
|
246
|
-
} = useChatProfilePopover();
|
|
247
|
+
} = useChatProfilePopover(focused ? user?.id : undefined);
|
|
247
248
|
|
|
248
249
|
const showUserProfilePopover = useCallback((targetUser: Parameters<typeof showProfilePopover>[0]) => {
|
|
249
250
|
showProfilePopover(targetUser, { ownProfile: targetUser.id === user?.id });
|
|
@@ -537,6 +538,8 @@ export function ChatContent({
|
|
|
537
538
|
loading={channelsLoading}
|
|
538
539
|
canManageNotifications={!!user?.emailVerified}
|
|
539
540
|
canCreateConversation={!!user?.emailVerified}
|
|
541
|
+
needsProfileSetup={!!user?.id && ownProfileConfigured === false}
|
|
542
|
+
onOpenProfile={openProfileSetup}
|
|
540
543
|
directExpanded={directExpanded}
|
|
541
544
|
onSelect={selectSidebarChannel}
|
|
542
545
|
onFocusRequest={() => setSidebarFocused(true)}
|
|
@@ -68,7 +68,13 @@ export class ChatControllerChannels {
|
|
|
68
68
|
|
|
69
69
|
const request = apiClient.getChannels()
|
|
70
70
|
.then((channels) => {
|
|
71
|
-
|
|
71
|
+
const publicChannels = normalizeChannels(channels);
|
|
72
|
+
this.channels = this.options.canLoadPrivateState()
|
|
73
|
+
? normalizeChannels([
|
|
74
|
+
...publicChannels,
|
|
75
|
+
...this.channels.filter((channel) => channel.kind === "direct" || channel.kind === "group"),
|
|
76
|
+
])
|
|
77
|
+
: publicChannels;
|
|
72
78
|
})
|
|
73
79
|
.catch(() => {
|
|
74
80
|
// Keep the last backend-provided list if the refresh fails.
|
|
@@ -73,15 +73,15 @@ export class ChatController {
|
|
|
73
73
|
private readonly session = createChatControllerSessionState();
|
|
74
74
|
private pendingMessageSeq = 0;
|
|
75
75
|
private notifyFn: (notification: AppNotificationRequest) => AppNotificationDelivery | void = () => {};
|
|
76
|
+
private openMessageFn: ((channelId: string, messageId: string) => void) | undefined;
|
|
76
77
|
private notifiedMessageIds = new Set<string>();
|
|
77
78
|
|
|
78
79
|
private readonly storage = new ChatControllerStorage({
|
|
79
80
|
emit: (channelId) => this.emit(channelId),
|
|
80
|
-
getSessionToken: () => this.session.sessionToken,
|
|
81
81
|
getUser: () => this.session.user,
|
|
82
82
|
});
|
|
83
83
|
private readonly channelCatalog = new ChatControllerChannels({
|
|
84
|
-
canLoadPrivateState: () => !!this.session.user?.emailVerified
|
|
84
|
+
canLoadPrivateState: () => !!this.session.user?.emailVerified,
|
|
85
85
|
ensureChannelState: (channelId) => this.ensureChannelState(channelId),
|
|
86
86
|
getChannelStateIds: () => this.storage.channelStates.keys(),
|
|
87
87
|
handleNotification: (notification, options) => this.handleChatNotification(notification, options),
|
|
@@ -94,7 +94,7 @@ export class ChatController {
|
|
|
94
94
|
getChannelStateSnapshots: () => this.channelCatalog.getChannelStateSnapshots(),
|
|
95
95
|
isChannelsLoading: () => this.channelCatalog.isLoading(),
|
|
96
96
|
isSessionChecked: () => this.session.sessionChecked,
|
|
97
|
-
|
|
97
|
+
hasSession: () => !!this.session.sessionToken || !!this.session.user,
|
|
98
98
|
getOnlineCount: () => this.channelCatalog.getOnlineCount(),
|
|
99
99
|
getUser: () => this.session.user,
|
|
100
100
|
getListenerSnapshot: (channelId) => this.getSnapshot(channelId),
|
|
@@ -109,7 +109,7 @@ export class ChatController {
|
|
|
109
109
|
});
|
|
110
110
|
private readonly realtime = new ChatControllerRealtime({
|
|
111
111
|
getAppActive: () => this.appActive,
|
|
112
|
-
|
|
112
|
+
hasSession: () => !!this.session.sessionToken || !!this.session.user,
|
|
113
113
|
getUser: () => this.session.user,
|
|
114
114
|
refreshSession: () => this.refreshSession(),
|
|
115
115
|
handleNotification: (notification) => this.handleChatNotification(notification),
|
|
@@ -126,8 +126,12 @@ export class ChatController {
|
|
|
126
126
|
this.hydrate();
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
setNotifier(
|
|
129
|
+
setNotifier(
|
|
130
|
+
notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void,
|
|
131
|
+
openMessage?: (channelId: string, messageId: string) => void,
|
|
132
|
+
): void {
|
|
130
133
|
this.notifyFn = notify;
|
|
134
|
+
this.openMessageFn = openMessage;
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
hydrate(): void {
|
|
@@ -375,6 +379,7 @@ export class ChatController {
|
|
|
375
379
|
flushDraftSync: (channelId) => this.storage.flushDraftSync(channelId),
|
|
376
380
|
resetNotifier: () => {
|
|
377
381
|
this.notifyFn = () => {};
|
|
382
|
+
this.openMessageFn = undefined;
|
|
378
383
|
},
|
|
379
384
|
stopRealtime: () => this.realtime.stopAll(),
|
|
380
385
|
});
|
|
@@ -393,7 +398,6 @@ export class ChatController {
|
|
|
393
398
|
content,
|
|
394
399
|
replyToId,
|
|
395
400
|
user: this.session.user,
|
|
396
|
-
sessionToken: this.session.sessionToken,
|
|
397
401
|
ensureConnection: () => this.ensureConnection(normalizedChannelId),
|
|
398
402
|
getVisibleMessages: () => this.getVisibleMessages(normalizedChannelId),
|
|
399
403
|
nextPendingMessageId: () => `local:${Date.now()}:${this.pendingMessageSeq += 1}`,
|
|
@@ -409,7 +413,7 @@ export class ChatController {
|
|
|
409
413
|
const channel = this.ensureChannelState(normalizedChannelId);
|
|
410
414
|
const messageContent = content.trim();
|
|
411
415
|
if (!messageContent) return false;
|
|
412
|
-
if (!this.session.user?.emailVerified
|
|
416
|
+
if (!this.session.user?.emailVerified) return false;
|
|
413
417
|
|
|
414
418
|
const latestOwnMessage = [...getVisibleMessages(channel)]
|
|
415
419
|
.reverse()
|
|
@@ -446,7 +450,7 @@ export class ChatController {
|
|
|
446
450
|
ensureChatChannelConnection({
|
|
447
451
|
channelId: normalizedChannelId,
|
|
448
452
|
channel,
|
|
449
|
-
canConnect: !!this.session.user?.emailVerified
|
|
453
|
+
canConnect: !!this.session.user?.emailVerified,
|
|
450
454
|
stopSafetyRefresh: () => this.realtime.stopSafetyRefresh(),
|
|
451
455
|
startSafetyRefresh: () => this.realtime.startSafetyRefresh(),
|
|
452
456
|
refreshMessages: () => this.refreshChannelMessages(normalizedChannelId),
|
|
@@ -529,6 +533,7 @@ export class ChatController {
|
|
|
529
533
|
getChannel: (channelId) => this.channelCatalog.getChannels().find((channel) => channel.id === channelId),
|
|
530
534
|
notifiedMessageIds: this.notifiedMessageIds,
|
|
531
535
|
notify: this.notifyFn,
|
|
536
|
+
openMessage: this.openMessageFn,
|
|
532
537
|
});
|
|
533
538
|
}
|
|
534
539
|
|
|
@@ -550,7 +555,7 @@ export class ChatController {
|
|
|
550
555
|
const channel = this.ensureChannelState(channelId);
|
|
551
556
|
return markChatChannelViewedThroughLatestMessage({
|
|
552
557
|
channel,
|
|
553
|
-
canSyncReadState: !!this.session.user?.emailVerified
|
|
558
|
+
canSyncReadState: !!this.session.user?.emailVerified,
|
|
554
559
|
persist,
|
|
555
560
|
persistChannelState: () => this.storage.persistChannelState(channelId),
|
|
556
561
|
syncReadState: (messageId) => {
|
|
@@ -20,11 +20,13 @@ function notifyChatServerMessage({
|
|
|
20
20
|
channel,
|
|
21
21
|
notifiedMessageIds,
|
|
22
22
|
notify,
|
|
23
|
+
openMessage,
|
|
23
24
|
}: {
|
|
24
25
|
notification: ChatNotification;
|
|
25
26
|
channel: ChatChannel | undefined;
|
|
26
27
|
notifiedMessageIds: Set<string>;
|
|
27
28
|
notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void;
|
|
29
|
+
openMessage?: (channelId: string, messageId: string) => void;
|
|
28
30
|
}): boolean {
|
|
29
31
|
if (notifiedMessageIds.has(notification.messageId)) return true;
|
|
30
32
|
const channelTitle = formatChatPaneTitle(channel, notification.channelId);
|
|
@@ -32,13 +34,18 @@ function notifyChatServerMessage({
|
|
|
32
34
|
? formatReplyToast(notification.message)
|
|
33
35
|
: notification.type === "mention"
|
|
34
36
|
? formatMentionToast(notification.message)
|
|
35
|
-
: formatChannelToast(
|
|
37
|
+
: formatChannelToast(notification.message, channel?.kind === "direct");
|
|
36
38
|
const delivered = wasNotificationDelivered(notify({
|
|
37
|
-
title:
|
|
38
|
-
subtitle: channelTitle,
|
|
39
|
+
title: channelTitle,
|
|
39
40
|
body,
|
|
40
41
|
type: "info",
|
|
41
42
|
desktop: "when-inactive",
|
|
43
|
+
...(openMessage ? {
|
|
44
|
+
action: {
|
|
45
|
+
label: "Open",
|
|
46
|
+
onClick: () => openMessage(notification.channelId, notification.messageId),
|
|
47
|
+
},
|
|
48
|
+
} : {}),
|
|
42
49
|
}));
|
|
43
50
|
if (delivered) {
|
|
44
51
|
notifiedMessageIds.add(notification.messageId);
|
|
@@ -55,6 +62,7 @@ export function handleChatNotification({
|
|
|
55
62
|
getChannel,
|
|
56
63
|
notifiedMessageIds,
|
|
57
64
|
notify,
|
|
65
|
+
openMessage,
|
|
58
66
|
}: {
|
|
59
67
|
notification: ChatNotification;
|
|
60
68
|
options?: { countUnread?: boolean };
|
|
@@ -64,6 +72,7 @@ export function handleChatNotification({
|
|
|
64
72
|
getChannel: (channelId: string) => ChatChannel | undefined;
|
|
65
73
|
notifiedMessageIds: Set<string>;
|
|
66
74
|
notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void;
|
|
75
|
+
openMessage?: (channelId: string, messageId: string) => void;
|
|
67
76
|
}): void {
|
|
68
77
|
mergeMessages(notification.channelId, [notification.message], { countUnread: options.countUnread });
|
|
69
78
|
const channel = ensureChannelState(notification.channelId);
|
|
@@ -73,6 +82,7 @@ export function handleChatNotification({
|
|
|
73
82
|
channel: getChannel(notification.channelId),
|
|
74
83
|
notifiedMessageIds,
|
|
75
84
|
notify,
|
|
85
|
+
openMessage,
|
|
76
86
|
});
|
|
77
87
|
if (activelyViewed) {
|
|
78
88
|
notifiedMessageIds.add(notification.messageId);
|
|
@@ -57,14 +57,12 @@ export function hydratePersistedChannelState({
|
|
|
57
57
|
persistence,
|
|
58
58
|
resume,
|
|
59
59
|
userId,
|
|
60
|
-
sessionToken,
|
|
61
60
|
}: {
|
|
62
61
|
channelId: string;
|
|
63
62
|
channel: ChannelRuntimeState;
|
|
64
63
|
persistence: PluginPersistence | null;
|
|
65
64
|
resume: PluginResumeState | null;
|
|
66
65
|
userId: string | null;
|
|
67
|
-
sessionToken: string | null;
|
|
68
66
|
}): void {
|
|
69
67
|
if (channel.hydrated || !persistence) return;
|
|
70
68
|
channel.hydrated = true;
|
|
@@ -84,7 +82,6 @@ export function hydratePersistedChannelState({
|
|
|
84
82
|
messages: transcript?.value.messages ?? [],
|
|
85
83
|
persistedChannel,
|
|
86
84
|
userId,
|
|
87
|
-
sessionToken,
|
|
88
85
|
});
|
|
89
86
|
}
|
|
90
87
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
|
|
7
7
|
interface ChatControllerRealtimeOptions {
|
|
8
8
|
getAppActive: () => boolean;
|
|
9
|
-
|
|
9
|
+
hasSession: () => boolean;
|
|
10
10
|
getUser: () => { emailVerified?: boolean } | null;
|
|
11
11
|
refreshSession: () => Promise<void>;
|
|
12
12
|
handleNotification: (notification: ChatNotification) => void;
|
|
@@ -26,7 +26,7 @@ export class ChatControllerRealtime {
|
|
|
26
26
|
|
|
27
27
|
syncVerificationPolling(): void {
|
|
28
28
|
const user = this.options.getUser();
|
|
29
|
-
if (!this.options.getAppActive() || !this.options.
|
|
29
|
+
if (!this.options.getAppActive() || !this.options.hasSession() || !user || user.emailVerified) {
|
|
30
30
|
this.stopVerificationPolling();
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
@@ -66,7 +66,7 @@ export class ChatControllerRealtime {
|
|
|
66
66
|
startSafetyRefresh(): void {
|
|
67
67
|
if (this.safetyRefreshTimer) return;
|
|
68
68
|
this.safetyRefreshTimer = setInterval(() => {
|
|
69
|
-
if (!this.options.getUser()?.emailVerified || !this.options.
|
|
69
|
+
if (!this.options.getUser()?.emailVerified || !this.options.hasSession()) {
|
|
70
70
|
this.stopSafetyRefresh();
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
@@ -14,7 +14,6 @@ interface SendChannelMessageOptions {
|
|
|
14
14
|
content: string;
|
|
15
15
|
replyToId?: string;
|
|
16
16
|
user: ChatSessionUser | null;
|
|
17
|
-
sessionToken: string | null;
|
|
18
17
|
ensureConnection: () => void;
|
|
19
18
|
getVisibleMessages: () => ChatMessage[];
|
|
20
19
|
nextPendingMessageId: () => string;
|
|
@@ -30,7 +29,6 @@ export function sendChatMessageToChannel({
|
|
|
30
29
|
content,
|
|
31
30
|
replyToId,
|
|
32
31
|
user,
|
|
33
|
-
sessionToken,
|
|
34
32
|
ensureConnection,
|
|
35
33
|
getVisibleMessages,
|
|
36
34
|
nextPendingMessageId,
|
|
@@ -41,7 +39,7 @@ export function sendChatMessageToChannel({
|
|
|
41
39
|
}: SendChannelMessageOptions): boolean {
|
|
42
40
|
const messageContent = content.trim();
|
|
43
41
|
if (!messageContent) return false;
|
|
44
|
-
if (!user?.emailVerified
|
|
42
|
+
if (!user?.emailVerified) return false;
|
|
45
43
|
if (hasPendingSend(channel, messageContent, replyToId)) return true;
|
|
46
44
|
if (!channel.wsConnection) {
|
|
47
45
|
ensureConnection();
|
|
@@ -46,13 +46,14 @@ export function hydrateChatControllerSession({
|
|
|
46
46
|
session.hydrated = true;
|
|
47
47
|
|
|
48
48
|
const storedSession = storage.readSessionState();
|
|
49
|
+
const restoredUser = apiClient.getCurrentUser() ?? storedSession?.user ?? null;
|
|
49
50
|
session.sessionToken = storedSession?.sessionToken ?? null;
|
|
50
51
|
apiClient.setSessionToken(session.sessionToken);
|
|
51
52
|
// WebSocket tokens are short-lived connection credentials. Reusing a persisted
|
|
52
53
|
// one can trap reconnects on an expired token even while the session cookie is valid.
|
|
53
54
|
apiClient.setWebSocketToken(null);
|
|
54
|
-
apiClient.restoreCachedUser(
|
|
55
|
-
session.user = normalizeSessionUser(
|
|
55
|
+
apiClient.restoreCachedUser(restoredUser);
|
|
56
|
+
session.user = normalizeSessionUser(restoredUser);
|
|
56
57
|
session.sessionChecked = true;
|
|
57
58
|
storage.ensureChannelState(DEFAULT_CHAT_CHANNEL_ID);
|
|
58
59
|
syncVerificationPolling();
|
|
@@ -115,17 +116,23 @@ export async function refreshChatControllerSession({
|
|
|
115
116
|
stopVerificationPolling,
|
|
116
117
|
syncVerificationPolling,
|
|
117
118
|
}: RefreshChatControllerSessionOptions): Promise<void> {
|
|
118
|
-
|
|
119
|
-
session.sessionToken = token;
|
|
120
|
-
if (!token) {
|
|
121
|
-
applySignedOut();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
119
|
+
session.sessionToken = apiClient.getSessionToken();
|
|
125
120
|
const apiSession = await apiClient.getSession();
|
|
126
121
|
if (!apiSession) {
|
|
127
|
-
apiClient.
|
|
128
|
-
session
|
|
122
|
+
const persistedToken = apiClient.getSessionToken() || session.sessionToken;
|
|
123
|
+
// A 200 with no user is what /auth/get-session returns when no cookie
|
|
124
|
+
// reached the server. Desktop's backend process used to treat that as a
|
|
125
|
+
// real sign-out and persist a wiped token, so the next launch was logged
|
|
126
|
+
// out. Keep a captured native session; hard account-missing responses
|
|
127
|
+
// already clear the token inside getSession().
|
|
128
|
+
if (persistedToken) {
|
|
129
|
+
session.sessionToken = persistedToken;
|
|
130
|
+
session.user = session.user ?? normalizeSessionUser(apiClient.getCurrentUser());
|
|
131
|
+
session.sessionChecked = true;
|
|
132
|
+
persistSession(session.sessionToken, session.user);
|
|
133
|
+
emit();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
129
136
|
applySignedOut();
|
|
130
137
|
return;
|
|
131
138
|
}
|
|
@@ -155,13 +155,11 @@ export function hydrateChannelRuntimeState({
|
|
|
155
155
|
messages,
|
|
156
156
|
persistedChannel,
|
|
157
157
|
userId,
|
|
158
|
-
sessionToken,
|
|
159
158
|
}: {
|
|
160
159
|
channel: ChannelRuntimeState;
|
|
161
160
|
messages: ChatMessage[];
|
|
162
161
|
persistedChannel: PersistedChannelState | null;
|
|
163
162
|
userId: string | null;
|
|
164
|
-
sessionToken: string | null;
|
|
165
163
|
}): void {
|
|
166
164
|
channel.draft = persistedChannel?.draft ?? "";
|
|
167
165
|
channel.draftClientMessageId = channel.draft.trim()
|
|
@@ -170,7 +168,7 @@ export function hydrateChannelRuntimeState({
|
|
|
170
168
|
channel.replyToId = persistedChannel?.replyToId ?? null;
|
|
171
169
|
channel.messages = messages;
|
|
172
170
|
channel.lastCursor = resolveHydratedCursor(messages, persistedChannel?.lastCursor ?? null);
|
|
173
|
-
channel.lastViewedMessageId = userId
|
|
171
|
+
channel.lastViewedMessageId = userId
|
|
174
172
|
? persistedChannel?.lastViewedMessageId ?? getLatestMessageId(messages)
|
|
175
173
|
: null;
|
|
176
174
|
}
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
|
|
23
23
|
interface ChatControllerStorageOptions {
|
|
24
24
|
emit: (channelId?: string) => void;
|
|
25
|
-
getSessionToken: () => string | null;
|
|
26
25
|
getUser: () => ChatSessionUser | null;
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -120,7 +119,6 @@ export class ChatControllerStorage {
|
|
|
120
119
|
persistence: this.persistence,
|
|
121
120
|
resume: this.resume,
|
|
122
121
|
userId: this.options.getUser()?.id ?? null,
|
|
123
|
-
sessionToken: this.options.getSessionToken(),
|
|
124
122
|
});
|
|
125
123
|
}
|
|
126
124
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ChatMessage } from "../../../../api-client";
|
|
2
2
|
import { t, tf } from "../../../../i18n";
|
|
3
3
|
|
|
4
4
|
const ISO_TIMESTAMP_CURSOR = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
@@ -43,13 +43,11 @@ export function formatReplyToast(message: ChatMessage): string {
|
|
|
43
43
|
return tf("@{author} replied to you: {snippet}", { author, snippet });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function formatChannelToast(
|
|
46
|
+
export function formatChannelToast(message: ChatMessage, direct = false): string {
|
|
47
47
|
const author = message.user.username || t("Someone");
|
|
48
48
|
const snippet = formatMessageSnippet(message.content);
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return snippet ? `${channelTitle} @${author}: ${snippet}` : tf("{channel} @{author} sent a message.", { channel: channelTitle, author });
|
|
49
|
+
if (direct && snippet) return snippet;
|
|
50
|
+
return snippet ? `@${author}: ${snippet}` : tf("@{author} sent a message.", { author });
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
export function isLegacyTimestampCursor(cursor: string | null): boolean {
|
|
@@ -16,7 +16,7 @@ interface ChatControllerViewOptions {
|
|
|
16
16
|
getChannelStateSnapshots: () => ChatChannelState[];
|
|
17
17
|
isChannelsLoading: () => boolean;
|
|
18
18
|
isSessionChecked: () => boolean;
|
|
19
|
-
|
|
19
|
+
hasSession: () => boolean;
|
|
20
20
|
getOnlineCount: () => number;
|
|
21
21
|
getUser: () => ChatControllerSnapshot["user"];
|
|
22
22
|
getListenerSnapshot: (channelId: string) => ChatControllerSnapshot;
|
|
@@ -69,7 +69,7 @@ export class ChatControllerView {
|
|
|
69
69
|
messagesError: channel.messagesError,
|
|
70
70
|
loadingOlderMessages: channel.olderMessagesLoading,
|
|
71
71
|
hasOlderMessages: channel.messages.length > 0 && !channel.reachedOldestMessage,
|
|
72
|
-
hasSavedSession: this.options.
|
|
72
|
+
hasSavedSession: this.options.hasSession(),
|
|
73
73
|
onlineCount: this.options.getOnlineCount(),
|
|
74
74
|
user: this.options.getUser(),
|
|
75
75
|
messages: this.options.getVisibleMessages(normalizedChannelId),
|