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
|
@@ -93,6 +93,7 @@ beforeEach(() => {
|
|
|
93
93
|
afterEach(() => {
|
|
94
94
|
apiClient.dispose();
|
|
95
95
|
apiClient.setSessionToken(null);
|
|
96
|
+
apiClient.setCookieSessionMode(false);
|
|
96
97
|
apiClient.connectChannel = originalConnectChannel;
|
|
97
98
|
apiClient.getSession = originalGetSession;
|
|
98
99
|
apiClient.getMessages = originalGetMessages;
|
|
@@ -147,6 +148,47 @@ describe("ChatController", () => {
|
|
|
147
148
|
expect(snapshot.messages.map((entry) => entry.id)).toEqual(["m1"]);
|
|
148
149
|
});
|
|
149
150
|
|
|
151
|
+
test("uses a browser cookie session without exposing its token", async () => {
|
|
152
|
+
const persistence = new MemoryPersistence();
|
|
153
|
+
const controller = new ChatController();
|
|
154
|
+
const sentMessages: string[] = [];
|
|
155
|
+
|
|
156
|
+
apiClient.setCookieSessionMode(true);
|
|
157
|
+
apiClient.restoreCachedUser({
|
|
158
|
+
id: "u1",
|
|
159
|
+
username: "vince",
|
|
160
|
+
emailVerified: true,
|
|
161
|
+
});
|
|
162
|
+
apiClient.getSession = async () => apiClient.getCurrentUser();
|
|
163
|
+
apiClient.connectChannel = () => ({
|
|
164
|
+
send: async (content) => {
|
|
165
|
+
sentMessages.push(content);
|
|
166
|
+
return {
|
|
167
|
+
id: "m1",
|
|
168
|
+
channelId: "everyone",
|
|
169
|
+
content,
|
|
170
|
+
replyToId: null,
|
|
171
|
+
createdAt: "2026-08-23T00:00:00.000Z",
|
|
172
|
+
user: { id: "u1", username: "vince", displayName: "Vince" },
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
close: () => {},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
controller.attachPersistence(persistence);
|
|
179
|
+
await controller.refreshSession();
|
|
180
|
+
|
|
181
|
+
expect(apiClient.getSessionToken()).toBeNull();
|
|
182
|
+
expect(controller.getSnapshot()).toMatchObject({
|
|
183
|
+
hasSavedSession: true,
|
|
184
|
+
user: { id: "u1", username: "vince", emailVerified: true },
|
|
185
|
+
});
|
|
186
|
+
expect(controller.send("hello from the browser")).toBe(true);
|
|
187
|
+
await flushMicrotasks();
|
|
188
|
+
expect(sentMessages).toEqual(["hello from the browser"]);
|
|
189
|
+
controller.dispose();
|
|
190
|
+
});
|
|
191
|
+
|
|
150
192
|
test("rejects unknown shortcut channels after the server list loads", async () => {
|
|
151
193
|
const controller = new ChatController();
|
|
152
194
|
apiClient.getChannels = async () => SERVER_CHAT_CHANNELS;
|
|
@@ -157,6 +199,42 @@ describe("ChatController", () => {
|
|
|
157
199
|
);
|
|
158
200
|
});
|
|
159
201
|
|
|
202
|
+
test("keeps private channels when a public refresh finishes after chat state", async () => {
|
|
203
|
+
const persistence = new MemoryPersistence();
|
|
204
|
+
const controller = new ChatController();
|
|
205
|
+
const directChannel: ChatChannel = {
|
|
206
|
+
id: "dm:u2",
|
|
207
|
+
name: "u2",
|
|
208
|
+
kind: "direct",
|
|
209
|
+
created_at: "2026-03-28T00:00:00.000Z",
|
|
210
|
+
};
|
|
211
|
+
let resolvePublicChannels: ((channels: ChatChannel[]) => void) | undefined;
|
|
212
|
+
|
|
213
|
+
persistence.setState("session", {
|
|
214
|
+
sessionToken: "token-123",
|
|
215
|
+
user: { id: "u1", username: "vince", emailVerified: true },
|
|
216
|
+
}, { schemaVersion: 1 });
|
|
217
|
+
apiClient.getChannels = () => new Promise((resolve) => {
|
|
218
|
+
resolvePublicChannels = resolve;
|
|
219
|
+
});
|
|
220
|
+
apiClient.getChatState = async () => ({
|
|
221
|
+
channels: [...SERVER_CHAT_CHANNELS, directChannel],
|
|
222
|
+
onlineCount: 0,
|
|
223
|
+
channelStates: [],
|
|
224
|
+
notifications: [],
|
|
225
|
+
});
|
|
226
|
+
controller.attachPersistence(persistence);
|
|
227
|
+
|
|
228
|
+
const publicRefresh = controller.refreshChannels();
|
|
229
|
+
await controller.refreshChatState();
|
|
230
|
+
expect(controller.getChannels().map((channel) => channel.id)).toContain(directChannel.id);
|
|
231
|
+
|
|
232
|
+
resolvePublicChannels!(SERVER_CHAT_CHANNELS);
|
|
233
|
+
await publicRefresh;
|
|
234
|
+
|
|
235
|
+
expect(controller.getChannels().map((channel) => channel.id)).toContain(directChannel.id);
|
|
236
|
+
});
|
|
237
|
+
|
|
160
238
|
test("hydrates a cached verified user into the api client for offline use", async () => {
|
|
161
239
|
const persistence = new MemoryPersistence();
|
|
162
240
|
const controller = new ChatController();
|
|
@@ -375,6 +453,55 @@ describe("ChatController", () => {
|
|
|
375
453
|
}
|
|
376
454
|
});
|
|
377
455
|
|
|
456
|
+
test("keeps a persisted native session when get-session returns no user", async () => {
|
|
457
|
+
const persistence = new MemoryPersistence();
|
|
458
|
+
const controller = new ChatController();
|
|
459
|
+
|
|
460
|
+
persistence.setState("session", {
|
|
461
|
+
sessionToken: "token-123",
|
|
462
|
+
user: { id: "u1", username: "vince", emailVerified: true },
|
|
463
|
+
}, { schemaVersion: 1 });
|
|
464
|
+
|
|
465
|
+
controller.attachPersistence(persistence);
|
|
466
|
+
apiClient.getSession = async () => null;
|
|
467
|
+
|
|
468
|
+
await controller.refreshSession();
|
|
469
|
+
|
|
470
|
+
expect(apiClient.getSessionToken()).toBe("token-123");
|
|
471
|
+
expect(persistence.getState<{
|
|
472
|
+
sessionToken: string;
|
|
473
|
+
user: { id: string; username: string; emailVerified: boolean };
|
|
474
|
+
}>("session", { schemaVersion: 1 })).toEqual({
|
|
475
|
+
sessionToken: "token-123",
|
|
476
|
+
user: { id: "u1", username: "vince", emailVerified: true },
|
|
477
|
+
});
|
|
478
|
+
expect(controller.getSnapshot().user).toEqual({
|
|
479
|
+
id: "u1",
|
|
480
|
+
username: "vince",
|
|
481
|
+
emailVerified: true,
|
|
482
|
+
});
|
|
483
|
+
controller.dispose();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("signs out when get-session returns no user and no token is stored", async () => {
|
|
487
|
+
const persistence = new MemoryPersistence();
|
|
488
|
+
const controller = new ChatController();
|
|
489
|
+
|
|
490
|
+
persistence.setState("session", {
|
|
491
|
+
sessionToken: null,
|
|
492
|
+
user: { id: "u1", username: "vince", emailVerified: true },
|
|
493
|
+
}, { schemaVersion: 1 });
|
|
494
|
+
|
|
495
|
+
controller.attachPersistence(persistence);
|
|
496
|
+
apiClient.getSession = async () => null;
|
|
497
|
+
|
|
498
|
+
await controller.refreshSession();
|
|
499
|
+
|
|
500
|
+
expect(apiClient.getSessionToken()).toBeNull();
|
|
501
|
+
expect(controller.getSnapshot().user).toBeNull();
|
|
502
|
+
controller.dispose();
|
|
503
|
+
});
|
|
504
|
+
|
|
378
505
|
test("keeps the cached session when session refresh fails transiently", async () => {
|
|
379
506
|
const persistence = new MemoryPersistence();
|
|
380
507
|
const controller = new ChatController();
|
|
@@ -1021,8 +1148,7 @@ describe("ChatController", () => {
|
|
|
1021
1148
|
(controller as any).handleChatNotification(mentionNotification());
|
|
1022
1149
|
|
|
1023
1150
|
expect(notifications).toEqual([{
|
|
1024
|
-
title: "
|
|
1025
|
-
subtitle: "#everyone",
|
|
1151
|
+
title: "#everyone",
|
|
1026
1152
|
body: "@bob mentioned you: hey @vince",
|
|
1027
1153
|
type: "info",
|
|
1028
1154
|
desktop: "when-inactive",
|
|
@@ -1246,8 +1372,7 @@ describe("ChatController", () => {
|
|
|
1246
1372
|
unreadCount: 3,
|
|
1247
1373
|
});
|
|
1248
1374
|
expect(notifications).toEqual([{
|
|
1249
|
-
title: "
|
|
1250
|
-
subtitle: "#options",
|
|
1375
|
+
title: "#options",
|
|
1251
1376
|
body: "@bob replied to you: answering you",
|
|
1252
1377
|
type: "info",
|
|
1253
1378
|
desktop: "when-inactive",
|
|
@@ -1327,10 +1452,11 @@ describe("ChatController", () => {
|
|
|
1327
1452
|
expect(notifications).toHaveLength(1);
|
|
1328
1453
|
});
|
|
1329
1454
|
|
|
1330
|
-
test("
|
|
1455
|
+
test("opens a server-issued channel notification at its exact message", () => {
|
|
1331
1456
|
const persistence = new MemoryPersistence();
|
|
1332
1457
|
const controller = new ChatController();
|
|
1333
1458
|
const notifications: AppNotificationRequest[] = [];
|
|
1459
|
+
const openedMessages: string[] = [];
|
|
1334
1460
|
const message: ChatMessage = {
|
|
1335
1461
|
id: "m1",
|
|
1336
1462
|
channelId: "options",
|
|
@@ -1346,6 +1472,8 @@ describe("ChatController", () => {
|
|
|
1346
1472
|
}, { schemaVersion: 1 });
|
|
1347
1473
|
controller.setNotifier((entry) => {
|
|
1348
1474
|
notifications.push(entry);
|
|
1475
|
+
}, (channelId, messageId) => {
|
|
1476
|
+
openedMessages.push(`${channelId}:${messageId}`);
|
|
1349
1477
|
});
|
|
1350
1478
|
controller.attachPersistence(persistence);
|
|
1351
1479
|
|
|
@@ -1359,15 +1487,17 @@ describe("ChatController", () => {
|
|
|
1359
1487
|
} satisfies ChatNotification);
|
|
1360
1488
|
|
|
1361
1489
|
expect(notifications).toEqual([{
|
|
1362
|
-
title: "
|
|
1363
|
-
|
|
1364
|
-
body: "#options @bob: new option flow",
|
|
1490
|
+
title: "#options",
|
|
1491
|
+
body: "@bob: new option flow",
|
|
1365
1492
|
type: "info",
|
|
1366
1493
|
desktop: "when-inactive",
|
|
1494
|
+
action: expect.objectContaining({ label: "Open" }),
|
|
1367
1495
|
}]);
|
|
1496
|
+
notifications[0]?.action?.onClick();
|
|
1497
|
+
expect(openedMessages).toEqual(["options:m1"]);
|
|
1368
1498
|
});
|
|
1369
1499
|
|
|
1370
|
-
test("uses direct channel labels in server-issued notification
|
|
1500
|
+
test("uses direct channel labels in server-issued notification titles", async () => {
|
|
1371
1501
|
const persistence = new MemoryPersistence();
|
|
1372
1502
|
const controller = new ChatController();
|
|
1373
1503
|
const notifications: AppNotificationRequest[] = [];
|
|
@@ -1418,9 +1548,8 @@ describe("ChatController", () => {
|
|
|
1418
1548
|
} satisfies ChatNotification);
|
|
1419
1549
|
|
|
1420
1550
|
expect(notifications).toEqual([{
|
|
1421
|
-
title: "
|
|
1422
|
-
|
|
1423
|
-
body: "@bob: ping",
|
|
1551
|
+
title: "@bob",
|
|
1552
|
+
body: "ping",
|
|
1424
1553
|
type: "info",
|
|
1425
1554
|
desktop: "when-inactive",
|
|
1426
1555
|
}]);
|
|
@@ -1491,8 +1620,7 @@ describe("ChatController", () => {
|
|
|
1491
1620
|
} satisfies ChatNotification);
|
|
1492
1621
|
|
|
1493
1622
|
expect(notifications).toEqual([{
|
|
1494
|
-
title: "
|
|
1495
|
-
subtitle: "#options",
|
|
1623
|
+
title: "#options",
|
|
1496
1624
|
body: "@bob replied to you: reply without channel notify",
|
|
1497
1625
|
type: "info",
|
|
1498
1626
|
desktop: "when-inactive",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createOpenTuiTestRoot as createRoot } from "../../../../renderers/opentui/test-utils";
|
|
5
|
+
import { getThemeColors, syncTheme } from "../../../../theme/colors";
|
|
6
|
+
import { ThemeProvider } from "../../../../theme/theme-context";
|
|
7
|
+
import { DEFAULT_THEME } from "../../../../theme/themes";
|
|
8
|
+
import type { ChatMessage } from "../../../../api-client";
|
|
9
|
+
import { DesktopChatMessage } from "./desktop";
|
|
10
|
+
|
|
11
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
|
|
12
|
+
let root: ReturnType<typeof createRoot> | undefined;
|
|
13
|
+
const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
|
|
14
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
function rgba(hex: string): string {
|
|
17
|
+
const value = hex.slice(1);
|
|
18
|
+
return [0, 2, 4].map((offset) => parseInt(value.slice(offset, offset + 2), 16)).concat(255).join(",");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
if (root) {
|
|
23
|
+
await act(async () => {
|
|
24
|
+
root?.unmount();
|
|
25
|
+
await Promise.resolve();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
testSetup?.renderer.destroy();
|
|
29
|
+
root = undefined;
|
|
30
|
+
testSetup = undefined;
|
|
31
|
+
syncTheme(DEFAULT_THEME);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("DesktopChatMessage", () => {
|
|
35
|
+
test("updates memoized row colors when the theme changes", async () => {
|
|
36
|
+
const message: ChatMessage = {
|
|
37
|
+
id: "theme-message",
|
|
38
|
+
channelId: "everyone",
|
|
39
|
+
content: "theme message",
|
|
40
|
+
replyToId: null,
|
|
41
|
+
createdAt: "2026-08-25T12:00:00.000Z",
|
|
42
|
+
user: { id: "user-1", username: "vince", displayName: "Vince" },
|
|
43
|
+
};
|
|
44
|
+
const noop = () => {};
|
|
45
|
+
const messageElement = (
|
|
46
|
+
<DesktopChatMessage
|
|
47
|
+
msg={message}
|
|
48
|
+
index={0}
|
|
49
|
+
messages={[message]}
|
|
50
|
+
selectedIdx={0}
|
|
51
|
+
hoveredIdx={null}
|
|
52
|
+
canSend={false}
|
|
53
|
+
catalog={{}}
|
|
54
|
+
userByUsername={new Map()}
|
|
55
|
+
openTicker={noop}
|
|
56
|
+
onUserHover={noop}
|
|
57
|
+
onUserHoverEnd={noop}
|
|
58
|
+
beginReplyTo={noop}
|
|
59
|
+
beginEditMessage={() => false}
|
|
60
|
+
jumpToMessage={noop}
|
|
61
|
+
latestEditableMessageId={null}
|
|
62
|
+
registerMessageElement={noop}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
testSetup = await createTestRenderer({ width: 60, height: 4 });
|
|
67
|
+
root = createRoot(testSetup.renderer);
|
|
68
|
+
act(() => root?.render(<ThemeProvider themeId={DEFAULT_THEME}>{messageElement}</ThemeProvider>));
|
|
69
|
+
await act(async () => {
|
|
70
|
+
await testSetup?.renderOnce();
|
|
71
|
+
await testSetup?.renderOnce();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const nextThemeId = "github-light";
|
|
75
|
+
act(() => root?.render(<ThemeProvider themeId={nextThemeId}>{messageElement}</ThemeProvider>));
|
|
76
|
+
await act(async () => {
|
|
77
|
+
await testSetup?.renderOnce();
|
|
78
|
+
await testSetup?.renderOnce();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const bodySpan = testSetup.captureSpans().lines
|
|
82
|
+
.flatMap((line) => line.spans)
|
|
83
|
+
.find((span) => span.text.includes(message.content));
|
|
84
|
+
const palette = getThemeColors(nextThemeId);
|
|
85
|
+
expect(bodySpan?.fg.toInts().join(",")).toBe(rgba(palette.selectedText));
|
|
86
|
+
expect(bodySpan?.bg.toInts().join(",")).toBe(rgba(palette.selected));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
2
|
import { Box, Span, Text } from "../../../../ui";
|
|
3
3
|
import { hoverBg } from "../../../../theme/colors";
|
|
4
|
+
import { useThemeColors } from "../../../../theme/theme-context";
|
|
4
5
|
import { t } from "../../../../i18n";
|
|
5
6
|
import { useAppLanguage } from "../../../../i18n/react";
|
|
6
7
|
import { MESSAGE_ACTION_WIDTH, normalizeInlinePreview } from "../layout";
|
|
@@ -32,6 +33,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
32
33
|
registerMessageElement: (messageId: string, node: unknown | null) => void;
|
|
33
34
|
}) {
|
|
34
35
|
useAppLanguage();
|
|
36
|
+
const themeColors = useThemeColors();
|
|
35
37
|
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
36
38
|
const canEditMessage = msg.id === latestEditableMessageId;
|
|
37
39
|
const showInlineReplyAction = !state.grouped && canSend;
|
|
@@ -58,7 +60,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
58
60
|
data-gloom-role="chat-message"
|
|
59
61
|
data-gloom-chat-message-id={msg.id}
|
|
60
62
|
data-selected={state.isSelected ? "true" : "false"}
|
|
61
|
-
style={{ "--chat-hover-bg": hoverBg(), minWidth: 0 }}
|
|
63
|
+
style={{ "--chat-hover-bg": hoverBg(themeColors), minWidth: 0 }}
|
|
62
64
|
>
|
|
63
65
|
{msg.replyTo && (
|
|
64
66
|
<Box
|
|
@@ -2,11 +2,11 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { act, useEffect } from "react";
|
|
3
3
|
import {
|
|
4
4
|
apiClient,
|
|
5
|
-
type AccountProfile,
|
|
6
5
|
type ChatUserSummary,
|
|
7
6
|
} from "../../../../api-client";
|
|
8
7
|
import { testRender } from "../../../../renderers/opentui/test-utils";
|
|
9
8
|
import { Box, Text } from "../../../../ui";
|
|
9
|
+
import { makeAccountProfile } from "../test-harness";
|
|
10
10
|
import { useChatProfilePopover } from "../profile-popover";
|
|
11
11
|
import {
|
|
12
12
|
hasPublicChatProfileInfo,
|
|
@@ -36,34 +36,6 @@ function makeUser(overrides: Partial<ChatUserSummary>): ChatUserSummary {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function makeAccountProfile(overrides: Partial<AccountProfile> = {}): AccountProfile {
|
|
40
|
-
return {
|
|
41
|
-
id: "u1",
|
|
42
|
-
email: "vince@example.com",
|
|
43
|
-
emailVerified: true,
|
|
44
|
-
plan: "pro",
|
|
45
|
-
username: "vince",
|
|
46
|
-
name: "Vince",
|
|
47
|
-
company: "Gloom",
|
|
48
|
-
title: null,
|
|
49
|
-
bio: "Made Gloomberb",
|
|
50
|
-
profilePublic: true,
|
|
51
|
-
publicEmail: null,
|
|
52
|
-
xAccount: null,
|
|
53
|
-
sharedPortfolioId: "broker:ibkr:coldstart",
|
|
54
|
-
acceptUnknownDms: false,
|
|
55
|
-
chatEmailNotificationsEnabled: true,
|
|
56
|
-
portfolioAnalytics: null,
|
|
57
|
-
syncEnabled: true,
|
|
58
|
-
weeklyRoundupEnabled: true,
|
|
59
|
-
positionAlertsEnabled: true,
|
|
60
|
-
lastSyncAt: null,
|
|
61
|
-
lastRoundupEmailAt: null,
|
|
62
|
-
updatedAt: "2026-07-21T22:03:59.832Z",
|
|
63
|
-
...overrides,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
39
|
function OwnProfileHarness({ user }: { user: ChatUserSummary }) {
|
|
68
40
|
const {
|
|
69
41
|
profilePopoverUser,
|
|
@@ -23,8 +23,23 @@ function accountProfileToChatUser(profile: AccountProfile): ChatUserSummary {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
const OPTIONAL_PROFILE_FIELDS = [
|
|
27
|
+
"company",
|
|
28
|
+
"title",
|
|
29
|
+
"bio",
|
|
30
|
+
"publicEmail",
|
|
31
|
+
"xAccount",
|
|
32
|
+
"sharedPortfolioId",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
/** `profilePublic` is a visibility switch, not a completion test: any filled optional field counts as set up. */
|
|
36
|
+
export function isAccountProfileConfigured(profile: AccountProfile): boolean {
|
|
37
|
+
return OPTIONAL_PROFILE_FIELDS.some((field) => (profile[field] ?? "").trim().length > 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useChatProfilePopover(trackOwnProfileUserId?: string) {
|
|
27
41
|
const [profilePopoverUser, setProfilePopoverUser] = useState<ChatUserSummary | null>(null);
|
|
42
|
+
const [ownProfileConfigured, setOwnProfileConfigured] = useState<boolean | null>(null);
|
|
28
43
|
const profilePopoverCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
44
|
const ownProfileRef = useRef<ChatUserSummary | null>(null);
|
|
30
45
|
const ownProfileRequestRef = useRef<Promise<void> | null>(null);
|
|
@@ -50,34 +65,25 @@ export function useChatProfilePopover() {
|
|
|
50
65
|
}, PROFILE_POPOVER_CLOSE_DELAY_MS);
|
|
51
66
|
}, [cancelProfilePopoverClose]);
|
|
52
67
|
|
|
53
|
-
const
|
|
54
|
-
targetUser: ChatUserSummary,
|
|
55
|
-
options?: { ownProfile?: boolean },
|
|
56
|
-
) => {
|
|
57
|
-
const ownProfile = options?.ownProfile === true;
|
|
58
|
-
const cachedUser = ownProfile && ownProfileRef.current?.id === targetUser.id
|
|
59
|
-
? ownProfileRef.current
|
|
60
|
-
: targetUser;
|
|
61
|
-
if (!ownProfile && !hasPublicChatProfileInfo(cachedUser)) {
|
|
62
|
-
closeProfilePopover();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
cancelProfilePopoverClose();
|
|
66
|
-
setProfilePopoverUser(cachedUser);
|
|
67
|
-
|
|
68
|
+
const refreshOwnProfile = useCallback((expectedUserId?: string, force = false) => {
|
|
68
69
|
if (
|
|
69
|
-
!
|
|
70
|
-
|| !apiClient.getSessionToken()
|
|
70
|
+
!apiClient.isSignedIn()
|
|
71
71
|
|| ownProfileRequestRef.current
|
|
72
|
-
|| (
|
|
72
|
+
|| (
|
|
73
|
+
!force
|
|
74
|
+
&& ownProfileRef.current
|
|
75
|
+
&& (!expectedUserId || ownProfileRef.current.id === expectedUserId)
|
|
76
|
+
&& Date.now() - ownProfileLoadedAtRef.current < 10_000
|
|
77
|
+
)
|
|
73
78
|
) return;
|
|
74
79
|
const request = apiClient.getAccountProfile()
|
|
75
80
|
.then((profile) => {
|
|
76
|
-
if (!activeRef.current || profile.id !==
|
|
81
|
+
if (!activeRef.current || (expectedUserId && profile.id !== expectedUserId)) return;
|
|
77
82
|
const nextUser = accountProfileToChatUser(profile);
|
|
78
83
|
ownProfileRef.current = nextUser;
|
|
79
84
|
ownProfileLoadedAtRef.current = Date.now();
|
|
80
|
-
|
|
85
|
+
setOwnProfileConfigured(isAccountProfileConfigured(profile));
|
|
86
|
+
setProfilePopoverUser((current) => current?.id === profile.id ? nextUser : current);
|
|
81
87
|
})
|
|
82
88
|
.catch(() => {})
|
|
83
89
|
.finally(() => {
|
|
@@ -86,7 +92,24 @@ export function useChatProfilePopover() {
|
|
|
86
92
|
}
|
|
87
93
|
});
|
|
88
94
|
ownProfileRequestRef.current = request;
|
|
89
|
-
}, [
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const showProfilePopover = useCallback((
|
|
98
|
+
targetUser: ChatUserSummary,
|
|
99
|
+
options?: { ownProfile?: boolean },
|
|
100
|
+
) => {
|
|
101
|
+
const ownProfile = options?.ownProfile === true;
|
|
102
|
+
const cachedUser = ownProfile && ownProfileRef.current?.id === targetUser.id
|
|
103
|
+
? ownProfileRef.current
|
|
104
|
+
: targetUser;
|
|
105
|
+
if (!ownProfile && !hasPublicChatProfileInfo(cachedUser)) {
|
|
106
|
+
closeProfilePopover();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
cancelProfilePopoverClose();
|
|
110
|
+
setProfilePopoverUser(cachedUser);
|
|
111
|
+
if (ownProfile) refreshOwnProfile(targetUser.id);
|
|
112
|
+
}, [cancelProfilePopoverClose, closeProfilePopover, refreshOwnProfile]);
|
|
90
113
|
|
|
91
114
|
useEffect(() => {
|
|
92
115
|
activeRef.current = true;
|
|
@@ -96,9 +119,17 @@ export function useChatProfilePopover() {
|
|
|
96
119
|
};
|
|
97
120
|
}, [cancelProfilePopoverClose]);
|
|
98
121
|
|
|
122
|
+
// Force a refresh when the pane regains focus so returning from Account Management settles the answer.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!trackOwnProfileUserId) return;
|
|
125
|
+
if (ownProfileRef.current?.id !== trackOwnProfileUserId) setOwnProfileConfigured(null);
|
|
126
|
+
refreshOwnProfile(trackOwnProfileUserId, true);
|
|
127
|
+
}, [refreshOwnProfile, trackOwnProfileUserId]);
|
|
128
|
+
|
|
99
129
|
return {
|
|
100
130
|
cancelProfilePopoverClose,
|
|
101
131
|
closeProfilePopover,
|
|
132
|
+
ownProfileConfigured,
|
|
102
133
|
profilePopoverUser,
|
|
103
134
|
scheduleProfilePopoverClose,
|
|
104
135
|
showProfilePopover,
|
|
@@ -11,6 +11,10 @@ import { gloomberbCloudPlugin } from "../cloud";
|
|
|
11
11
|
import { ChatContent } from "./content";
|
|
12
12
|
import { chatController } from "./controller";
|
|
13
13
|
import { useChatChannelNavigation } from "./content/channel-navigation";
|
|
14
|
+
import {
|
|
15
|
+
subscribeRequestedAccountManagementTab,
|
|
16
|
+
type AccountManagementTab,
|
|
17
|
+
} from "../account-management/navigation";
|
|
14
18
|
import {
|
|
15
19
|
cleanupChatTest,
|
|
16
20
|
createChatTestControls,
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
installChatApiTestDefaults,
|
|
20
24
|
installServerChannels,
|
|
21
25
|
lineText,
|
|
26
|
+
makeAccountProfile,
|
|
22
27
|
makeMessage,
|
|
23
28
|
MemoryPersistence,
|
|
24
29
|
type ChatTestSetup,
|
|
@@ -312,6 +317,82 @@ describe("ChatContent channel sidebar", () => {
|
|
|
312
317
|
expect(setup().captureCharFrame()).toContain("● 6 online");
|
|
313
318
|
});
|
|
314
319
|
|
|
320
|
+
test("offers a sidebar profile shortcut only until the account profile is filled in", async () => {
|
|
321
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
322
|
+
installServerChannels(controller);
|
|
323
|
+
controller.refreshChannels = async () => {};
|
|
324
|
+
controller.refreshChannelMessages = async () => {};
|
|
325
|
+
const originalGetAccountProfile = apiClient.getAccountProfile.bind(apiClient);
|
|
326
|
+
apiClient.getAccountProfile = async () => makeAccountProfile({
|
|
327
|
+
id: "u0",
|
|
328
|
+
company: null,
|
|
329
|
+
title: null,
|
|
330
|
+
bio: null,
|
|
331
|
+
publicEmail: null,
|
|
332
|
+
xAccount: null,
|
|
333
|
+
// Set, and deliberately not part of the completion test.
|
|
334
|
+
profilePublic: true,
|
|
335
|
+
sharedPortfolioId: null,
|
|
336
|
+
});
|
|
337
|
+
const requestedTabs: AccountManagementTab[] = [];
|
|
338
|
+
const unsubscribe = subscribeRequestedAccountManagementTab((tab) => requestedTabs.push(tab));
|
|
339
|
+
const shownPanes: string[] = [];
|
|
340
|
+
const runtime = createTestPluginRuntime({
|
|
341
|
+
showPane: (paneId: string) => {
|
|
342
|
+
shownPanes.push(paneId);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
await act(async () => {
|
|
348
|
+
testSetup = await testRender(createHarness(controller, { width: 90, height: 14, runtime }), {
|
|
349
|
+
width: 90,
|
|
350
|
+
height: 14,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
await flushFrame();
|
|
354
|
+
await flushFrame();
|
|
355
|
+
|
|
356
|
+
const lines = setup().captureCharFrame().split("\n");
|
|
357
|
+
const row = lines.findIndex((line) => line.includes("@ Profile"));
|
|
358
|
+
const col = lines[row]?.indexOf("Profile") ?? -1;
|
|
359
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
360
|
+
|
|
361
|
+
await act(async () => {
|
|
362
|
+
await setup().mockMouse.click(col + 1, row);
|
|
363
|
+
await setup().renderOnce();
|
|
364
|
+
await setup().renderOnce();
|
|
365
|
+
});
|
|
366
|
+
await flushFrame();
|
|
367
|
+
|
|
368
|
+
expect(requestedTabs).toEqual(["profile"]);
|
|
369
|
+
expect(shownPanes).toEqual(["account-management"]);
|
|
370
|
+
|
|
371
|
+
apiClient.getAccountProfile = async () => makeAccountProfile({
|
|
372
|
+
id: "u0",
|
|
373
|
+
company: null,
|
|
374
|
+
title: "Trader",
|
|
375
|
+
bio: null,
|
|
376
|
+
profilePublic: false,
|
|
377
|
+
sharedPortfolioId: null,
|
|
378
|
+
});
|
|
379
|
+
await act(async () => {
|
|
380
|
+
testSetup?.renderer.destroy();
|
|
381
|
+
testSetup = await testRender(createHarness(controller, { width: 90, height: 14, runtime }), {
|
|
382
|
+
width: 90,
|
|
383
|
+
height: 14,
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
await flushFrame();
|
|
387
|
+
await flushFrame();
|
|
388
|
+
|
|
389
|
+
expect(setup().captureCharFrame()).not.toContain("@ Profile");
|
|
390
|
+
} finally {
|
|
391
|
+
unsubscribe();
|
|
392
|
+
apiClient.getAccountProfile = originalGetAccountProfile;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
315
396
|
test("uses arrows to move between channel sidebar and chat content", async () => {
|
|
316
397
|
const controller = createController({ sessionToken: "token-123" });
|
|
317
398
|
installServerChannels(controller);
|