gloomberb 0.10.0 → 0.10.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/package.json +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
- package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PricePoint } from "../../../types/financials";
|
|
2
|
+
import { getPricePointTimestamp } from "../../../utils/price-history";
|
|
2
3
|
|
|
3
4
|
export interface DatedReturn {
|
|
4
5
|
dateKey: string;
|
|
@@ -10,21 +11,14 @@ export interface CorrelationResult {
|
|
|
10
11
|
sampleSize: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function getPointTimestamp(point: PricePoint): number {
|
|
14
|
-
const value = point.date as Date | string | number | null | undefined;
|
|
15
|
-
if (value instanceof Date) return value.getTime();
|
|
16
|
-
if (value == null) return Number.NaN;
|
|
17
|
-
return new Date(value).getTime();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
14
|
function toDateKey(point: PricePoint): string | null {
|
|
21
|
-
const timestamp =
|
|
15
|
+
const timestamp = getPricePointTimestamp(point);
|
|
22
16
|
if (!Number.isFinite(timestamp)) return null;
|
|
23
17
|
return new Date(timestamp).toISOString().slice(0, 10);
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
function comparePricePointsByDate(left: PricePoint, right: PricePoint): number {
|
|
27
|
-
return
|
|
21
|
+
return getPricePointTimestamp(left) - getPricePointTimestamp(right);
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
export function computeReturns(closes: number[]): number[] {
|
|
@@ -40,7 +34,7 @@ export function computeReturns(closes: number[]): number[] {
|
|
|
40
34
|
|
|
41
35
|
export function computeDatedReturns(points: PricePoint[]): DatedReturn[] {
|
|
42
36
|
const sorted = [...points]
|
|
43
|
-
.filter((point) => Number.isFinite(
|
|
37
|
+
.filter((point) => Number.isFinite(getPricePointTimestamp(point)) && Number.isFinite(point.close))
|
|
44
38
|
.sort(comparePricePointsByDate);
|
|
45
39
|
const byDate = new Map<string, number>();
|
|
46
40
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box } from "../../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
4
|
-
import { DataTableStackView,
|
|
4
|
+
import { DataTableStackView, type DataTableCell, type PaneFooterSegment } from "../../../components";
|
|
5
5
|
import type { PaneProps } from "../../../types/plugin";
|
|
6
6
|
import type { PluginModule } from "../plugin-module";
|
|
7
7
|
import { colors, blendHex } from "../../../theme/colors";
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type ImpactFilter,
|
|
31
31
|
} from "./calendar-model";
|
|
32
32
|
import { attachFredSeriesPersistence } from "../../../data/fred-series";
|
|
33
|
+
import { usePaneStatusFooter } from "../shared/pane-footer";
|
|
33
34
|
|
|
34
35
|
function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
35
36
|
const [initialCache] = useState(() => getCalendarCache());
|
|
@@ -221,24 +222,21 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
221
222
|
].filter(Boolean).join(" · ") || undefined
|
|
222
223
|
: undefined;
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{ id: "refresh", key: "r", label: "efresh", onPress: () => load(true) },
|
|
240
|
-
],
|
|
241
|
-
}), [countryFilter, cycleCountryFilter, cycleImpactFilter, error, impactFilter, load, loading, nextCountdown, nextEvent?.event, staleness]);
|
|
225
|
+
const calendarStatus = useMemo<PaneFooterSegment[]>(() => [
|
|
226
|
+
...(impactFilter !== "all" ? [{ id: "impact", parts: [{ text: `impact: ${impactFilter}`, tone: "value" as const }] }] : []),
|
|
227
|
+
...(countryFilter !== "all" ? [{ id: "country", parts: [{ text: `country: ${countryFilter}`, tone: "value" as const }] }] : []),
|
|
228
|
+
...(nextEvent && nextCountdown ? [{
|
|
229
|
+
id: "next",
|
|
230
|
+
parts: [{ text: `Next: ${nextEvent.event.length > 18 ? nextEvent.event.slice(0, 18).trimEnd() : nextEvent.event} ${nextCountdown}`, tone: "muted" as const }],
|
|
231
|
+
}] : []),
|
|
232
|
+
...(staleness ? [{ id: "stale", parts: [{ text: staleness, tone: "muted" as const }] }] : []),
|
|
233
|
+
], [countryFilter, impactFilter, nextCountdown, nextEvent?.event, staleness]);
|
|
234
|
+
usePaneStatusFooter({
|
|
235
|
+
registrationId: "econ-calendar",
|
|
236
|
+
loading,
|
|
237
|
+
error,
|
|
238
|
+
info: calendarStatus,
|
|
239
|
+
});
|
|
242
240
|
|
|
243
241
|
const handleHeaderClick = useCallback(() => {}, []);
|
|
244
242
|
const openDisplayRow = useCallback((row: DisplayRow) => {
|
|
@@ -5,7 +5,6 @@ export type ViewMode = "table" | "chart";
|
|
|
5
5
|
export type HolderColumnId = "holder" | "value" | "shares" | "changeShares" | "changePercent" | "percentHeld" | "reportDate";
|
|
6
6
|
export type HolderColumn = DataTableColumn & { id: HolderColumnId };
|
|
7
7
|
export type SortDirection = "asc" | "desc";
|
|
8
|
-
export type PreventableMouseEvent = { preventDefault(): void };
|
|
9
8
|
|
|
10
9
|
export interface SortPreference {
|
|
11
10
|
columnId: HolderColumnId;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Text } from "../../../ui";
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
3
|
import type { PluginModule } from "../plugin-module";
|
|
4
4
|
import type { SecFilingItem } from "../../../types/data-provider";
|
|
5
5
|
import {
|
|
6
6
|
useResolvedEntryValue,
|
|
7
|
-
useSecFilingContent,
|
|
8
7
|
useSecFilingsQuery,
|
|
9
8
|
} from "../../../market-data/hooks";
|
|
10
9
|
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
formatFilingShortDate,
|
|
25
24
|
renderFilingNotice,
|
|
26
25
|
} from "../sec/filing-display";
|
|
26
|
+
import { useSecFilingContentCache } from "../sec/filing-content";
|
|
27
27
|
|
|
28
28
|
const FORM4_LIMIT = 20;
|
|
29
29
|
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
@@ -125,34 +125,10 @@ function InsiderView({ width, height, focused }: { width: number; height: number
|
|
|
125
125
|
? (filingsEntry.error?.message ?? "Failed to load SEC filings")
|
|
126
126
|
: null;
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Reset when ticker changes
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
setContentMap(new Map());
|
|
135
|
-
setFetchPointer(0);
|
|
136
|
-
}, [ticker?.metadata.ticker]);
|
|
137
|
-
|
|
138
|
-
// Pick the next un-fetched filing to load
|
|
139
|
-
const nextToFetch = form4Filings.find((f) => !contentMap.has(f.accessionNumber)) ?? null;
|
|
140
|
-
const contentEntry = useSecFilingContent(nextToFetch);
|
|
141
|
-
const contentValue = useResolvedEntryValue(contentEntry);
|
|
142
|
-
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
if (!nextToFetch) return;
|
|
145
|
-
if (contentEntry?.phase === "error") {
|
|
146
|
-
setContentMap((prev) => new Map(prev).set(nextToFetch.accessionNumber, null));
|
|
147
|
-
setFetchPointer((p) => p + 1);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (contentValue !== null && contentValue !== undefined) {
|
|
151
|
-
setContentMap((prev) => new Map(prev).set(nextToFetch.accessionNumber, contentValue));
|
|
152
|
-
setFetchPointer((p) => p + 1);
|
|
153
|
-
}
|
|
154
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
155
|
-
}, [contentValue, contentEntry?.phase, nextToFetch?.accessionNumber, fetchPointer]);
|
|
128
|
+
const { contentCache: contentMap, pendingCount } = useSecFilingContentCache({
|
|
129
|
+
scopeKey: `${ticker?.metadata.ticker ?? "none"}:${ticker?.metadata.exchange ?? ""}`,
|
|
130
|
+
targets: form4Filings,
|
|
131
|
+
});
|
|
156
132
|
|
|
157
133
|
const allParsed: ParsedFiling[] = useMemo(() => form4Filings.map((filing) => {
|
|
158
134
|
const hasContent = contentMap.has(filing.accessionNumber);
|
|
@@ -172,7 +148,6 @@ function InsiderView({ width, height, focused }: { width: number; height: number
|
|
|
172
148
|
), [allParsed, nameFilter]);
|
|
173
149
|
const feedItems = useMemo(() => toFeedItems(parsed), [parsed]);
|
|
174
150
|
const summary = useMemo(() => buildSummary(allParsed), [allParsed]);
|
|
175
|
-
const pendingCount = form4Filings.filter((f) => !contentMap.has(f.accessionNumber)).length;
|
|
176
151
|
const selectedTransaction = parsed[selectedIdx]?.transaction ?? null;
|
|
177
152
|
const openFiling = openItemId
|
|
178
153
|
? parsed.find(({ filing }) => filing.accessionNumber === openItemId)?.filing ?? null
|
|
@@ -107,17 +107,15 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
|
|
|
107
107
|
}
|
|
108
108
|
}, [news.length, selectedIdx, setSelectedIdx]);
|
|
109
109
|
|
|
110
|
-
const footerInfo = useMemo(() => [
|
|
111
|
-
...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
|
|
112
|
-
...(error ? [{ id: "error", parts: [{ text: "error", tone: "warning" as const }] }] : []),
|
|
113
|
-
...(loadingSummary ? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }] : []),
|
|
114
|
-
], [error, loading, loadingSummary]);
|
|
115
|
-
|
|
116
110
|
useNewsArticleFooter({
|
|
117
111
|
registrationId: "news",
|
|
118
112
|
focused,
|
|
119
113
|
article: openArticle,
|
|
120
|
-
|
|
114
|
+
loading,
|
|
115
|
+
error,
|
|
116
|
+
info: loadingSummary
|
|
117
|
+
? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
|
|
118
|
+
: undefined,
|
|
121
119
|
});
|
|
122
120
|
|
|
123
121
|
if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view news.</Text>;
|
|
@@ -10,7 +10,7 @@ import { createStatefulTestPluginRuntime } from "../../../../../test-support/plu
|
|
|
10
10
|
import { createDefaultConfig } from "../../../../../types/config";
|
|
11
11
|
import { Box } from "../../../../../ui";
|
|
12
12
|
import { PluginRenderProvider } from "../../../../runtime";
|
|
13
|
-
import {
|
|
13
|
+
import { setDetectedProviders } from "../../../ai/providers";
|
|
14
14
|
import { setAiRunHost } from "../../../ai/runner";
|
|
15
15
|
import { BreakingPane } from "./pane";
|
|
16
16
|
|
|
@@ -53,18 +53,16 @@ function createReadyNewsService(articles: NewsArticle[]): { service: NewsService
|
|
|
53
53
|
updatedAt: Date.now(),
|
|
54
54
|
sourceIds: ["test"],
|
|
55
55
|
};
|
|
56
|
-
const listeners = new Set<() => void>();
|
|
56
|
+
const listeners = new Set<(state: NewsQueryState) => void>();
|
|
57
57
|
let queryStateCalls = 0;
|
|
58
58
|
const service = {
|
|
59
|
-
|
|
59
|
+
watchQuery(_query: unknown, listener: (state: NewsQueryState) => void) {
|
|
60
60
|
listeners.add(listener);
|
|
61
|
+
listener(state);
|
|
61
62
|
return () => {
|
|
62
63
|
listeners.delete(listener);
|
|
63
64
|
};
|
|
64
65
|
},
|
|
65
|
-
getVersion() {
|
|
66
|
-
return 1;
|
|
67
|
-
},
|
|
68
66
|
getQueryState() {
|
|
69
67
|
queryStateCalls += 1;
|
|
70
68
|
return state;
|
|
@@ -110,7 +108,7 @@ function createHarness() {
|
|
|
110
108
|
afterEach(async () => {
|
|
111
109
|
setSharedNewsService(null);
|
|
112
110
|
setAiRunHost(null);
|
|
113
|
-
|
|
111
|
+
setDetectedProviders(null);
|
|
114
112
|
if (testSetup) {
|
|
115
113
|
await act(async () => {
|
|
116
114
|
testSetup!.renderer.destroy();
|
|
@@ -124,7 +122,7 @@ describe("BreakingPane", () => {
|
|
|
124
122
|
let runCalls = 0;
|
|
125
123
|
const newsService = createReadyNewsService([makeArticle()]);
|
|
126
124
|
setSharedNewsService(newsService.service);
|
|
127
|
-
|
|
125
|
+
setDetectedProviders([
|
|
128
126
|
{
|
|
129
127
|
id: "anthropic",
|
|
130
128
|
name: "Claude",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { PaneFooterSegment } from "../../../../../components";
|
|
3
3
|
import { useShortcut } from "../../../../../react/input";
|
|
4
4
|
import { useRendererHost } from "../../../../../ui";
|
|
5
5
|
import { apiClient } from "../../../../../api-client";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { usePaneStatusLinkFooter } from "../../../shared/pane-footer";
|
|
7
|
+
import { CLOUD_UPGRADE_URL } from "../../../shared/cloud-upgrade";
|
|
8
8
|
|
|
9
9
|
interface NewsFooterArticle {
|
|
10
10
|
source?: string | null;
|
|
@@ -16,7 +16,8 @@ interface UseNewsArticleFooterOptions {
|
|
|
16
16
|
focused: boolean;
|
|
17
17
|
article: NewsFooterArticle | null | undefined;
|
|
18
18
|
info?: PaneFooterSegment[];
|
|
19
|
-
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
error?: string | null;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function hasRealtimeNewsAccess(): boolean {
|
|
@@ -29,7 +30,8 @@ export function useNewsArticleFooter({
|
|
|
29
30
|
focused,
|
|
30
31
|
article,
|
|
31
32
|
info,
|
|
32
|
-
|
|
33
|
+
loading = false,
|
|
34
|
+
error,
|
|
33
35
|
}: UseNewsArticleFooterOptions) {
|
|
34
36
|
const rendererHost = useRendererHost();
|
|
35
37
|
const hasRealtimeAccess = hasRealtimeNewsAccess();
|
|
@@ -60,18 +62,15 @@ export function useNewsArticleFooter({
|
|
|
60
62
|
parts: [{ text: "delayed 12h, upgrade for realtime", tone: "warning" }],
|
|
61
63
|
}]
|
|
62
64
|
), [hasRealtimeAccess, openUpgrade, showAccessFooter]);
|
|
63
|
-
const upgradeHints = useMemo<PaneHint[]>(() => (
|
|
64
|
-
showAccessFooter && !hasRealtimeAccess ? [{ id: "upgrade", key: "u", label: "pgrade", onPress: openUpgrade }] : []
|
|
65
|
-
), [hasRealtimeAccess, openUpgrade, showAccessFooter]);
|
|
66
65
|
const footerInfo = useMemo(() => [...accessInfo, ...(info ?? [])], [accessInfo, info]);
|
|
67
|
-
const footerHints = useMemo(() => [...upgradeHints, ...(hints ?? [])], [hints, upgradeHints]);
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
usePaneStatusLinkFooter({
|
|
70
68
|
registrationId,
|
|
71
69
|
focused,
|
|
72
70
|
url: article?.url,
|
|
73
71
|
source: article?.source,
|
|
74
72
|
info: footerInfo,
|
|
75
|
-
|
|
73
|
+
loading,
|
|
74
|
+
error,
|
|
76
75
|
});
|
|
77
76
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
MAX_READ_ARTICLE_IDS,
|
|
4
4
|
markNewsArticleRead,
|
|
5
5
|
normalizeNewsReadState,
|
|
6
|
+
type NewsReadState,
|
|
6
7
|
} from "./read-state";
|
|
7
8
|
|
|
8
9
|
describe("news read state", () => {
|
|
@@ -27,4 +28,10 @@ describe("news read state", () => {
|
|
|
27
28
|
expect(state.articleIds[0]).toBe("id-0");
|
|
28
29
|
expect(state.articleIds.at(-1)).toBe(`id-${MAX_READ_ARTICLE_IDS - 1}`);
|
|
29
30
|
});
|
|
31
|
+
|
|
32
|
+
test("repairs persisted state with a missing article list", () => {
|
|
33
|
+
const state = normalizeNewsReadState({} as NewsReadState);
|
|
34
|
+
|
|
35
|
+
expect(state).toEqual({ articleIds: [] });
|
|
36
|
+
});
|
|
30
37
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react";
|
|
2
|
-
import { usePluginState } from "../../../runtime";
|
|
3
1
|
import {
|
|
4
2
|
DEFAULT_MAX_READ_IDS,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
markPersistedReadId,
|
|
4
|
+
normalizePersistedReadIdState,
|
|
5
|
+
usePersistedReadIds,
|
|
6
|
+
type PersistedReadIdAdapter,
|
|
8
7
|
} from "../../shared/read-state";
|
|
9
8
|
|
|
10
9
|
export interface NewsReadState {
|
|
@@ -16,38 +15,29 @@ export const MAX_READ_ARTICLE_IDS = DEFAULT_MAX_READ_IDS;
|
|
|
16
15
|
|
|
17
16
|
const READ_STATE_KEY = "read-articles";
|
|
18
17
|
const EMPTY_READ_STATE: NewsReadState = { articleIds: [] };
|
|
18
|
+
const NEWS_READ_STATE_ADAPTER: PersistedReadIdAdapter<NewsReadState> = {
|
|
19
|
+
getIds: (state) => state.articleIds,
|
|
20
|
+
withIds: (_state, articleIds) => ({ articleIds }),
|
|
21
|
+
maxIds: MAX_READ_ARTICLE_IDS,
|
|
22
|
+
};
|
|
19
23
|
|
|
20
24
|
export function normalizeNewsReadState(state: NewsReadState): NewsReadState {
|
|
21
|
-
return
|
|
25
|
+
return normalizePersistedReadIdState(state, NEWS_READ_STATE_ADAPTER);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function markNewsArticleRead(
|
|
25
29
|
state: NewsReadState,
|
|
26
30
|
articleId: string,
|
|
27
31
|
): NewsReadState {
|
|
28
|
-
|
|
29
|
-
const articleIds = markReadId(current, articleId, MAX_READ_ARTICLE_IDS);
|
|
30
|
-
|
|
31
|
-
return sameStringArray(articleIds, state.articleIds) ? state : { articleIds };
|
|
32
|
+
return markPersistedReadId(state, articleId, NEWS_READ_STATE_ADAPTER);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function useNewsReadState() {
|
|
35
|
-
const
|
|
36
|
-
READ_STATE_KEY,
|
|
37
|
-
EMPTY_READ_STATE,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
[readState],
|
|
43
|
-
);
|
|
44
|
-
const readArticleIds = useMemo(
|
|
45
|
-
() => new Set(normalizedReadState.articleIds),
|
|
46
|
-
[normalizedReadState],
|
|
47
|
-
);
|
|
48
|
-
const markArticleRead = useCallback((articleId: string) => {
|
|
49
|
-
setReadState((current) => markNewsArticleRead(current, articleId));
|
|
50
|
-
}, [setReadState]);
|
|
51
|
-
|
|
52
|
-
return { readArticleIds, markArticleRead };
|
|
36
|
+
const { readIds, markRead } = usePersistedReadIds({
|
|
37
|
+
key: READ_STATE_KEY,
|
|
38
|
+
fallback: EMPTY_READ_STATE,
|
|
39
|
+
schemaVersion: NEWS_READ_STATE_SCHEMA_VERSION,
|
|
40
|
+
adapter: NEWS_READ_STATE_ADAPTER,
|
|
41
|
+
});
|
|
42
|
+
return { readArticleIds: readIds, markArticleRead: markRead };
|
|
53
43
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MarketNewsItem } from "../../../../../types/news-source";
|
|
2
|
+
import { decodeHtmlEntities } from "../../../../../utils/html-entities";
|
|
2
3
|
import { hashString } from "../hash";
|
|
3
4
|
|
|
4
5
|
export interface RssFeedConfig {
|
|
@@ -10,17 +11,6 @@ export interface RssFeedConfig {
|
|
|
10
11
|
enabled: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function decodeHtmlEntities(s: string): string {
|
|
14
|
-
return s
|
|
15
|
-
.replace(/&/g, "&")
|
|
16
|
-
.replace(/</g, "<")
|
|
17
|
-
.replace(/>/g, ">")
|
|
18
|
-
.replace(/"/g, '"')
|
|
19
|
-
.replace(/'/g, "'")
|
|
20
|
-
.replace(/'/g, "'")
|
|
21
|
-
.replace(/ /g, " ");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
function stripCdata(s: string): string {
|
|
25
15
|
return s.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, inner) => inner);
|
|
26
16
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { OptionsChain } from "../../../types/financials";
|
|
3
|
+
import { buildOptionsAccessFooterSegment, resolveOptionsAccessFooterState } from "./footer";
|
|
4
|
+
|
|
5
|
+
function chain(overrides: Partial<OptionsChain> = {}): OptionsChain {
|
|
6
|
+
return {
|
|
7
|
+
underlyingSymbol: "AAPL",
|
|
8
|
+
expirationDates: [],
|
|
9
|
+
calls: [],
|
|
10
|
+
puts: [],
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("options access footer", () => {
|
|
16
|
+
test("offers free users the exact delayed upgrade state with a clickable segment", () => {
|
|
17
|
+
const state = resolveOptionsAccessFooterState({
|
|
18
|
+
chain: chain({
|
|
19
|
+
dataSource: "delayed",
|
|
20
|
+
delayMinutes: 15,
|
|
21
|
+
realtimeEligible: false,
|
|
22
|
+
}),
|
|
23
|
+
clientPlan: "free",
|
|
24
|
+
hasLiveQuote: false,
|
|
25
|
+
});
|
|
26
|
+
let upgrades = 0;
|
|
27
|
+
const segment = buildOptionsAccessFooterSegment(state, () => {
|
|
28
|
+
upgrades += 1;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(state).toEqual({
|
|
32
|
+
canUpgrade: true,
|
|
33
|
+
text: "15-minute delayed options, upgrade for real-time",
|
|
34
|
+
tone: "warning",
|
|
35
|
+
});
|
|
36
|
+
segment.onPress?.();
|
|
37
|
+
expect(upgrades).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("claims real-time only for a Pro user with a fresh live stream", () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveOptionsAccessFooterState({
|
|
43
|
+
chain: chain({
|
|
44
|
+
dataSource: "delayed",
|
|
45
|
+
delayMinutes: 15,
|
|
46
|
+
realtimeEligible: true,
|
|
47
|
+
}),
|
|
48
|
+
clientPlan: "pro",
|
|
49
|
+
hasLiveQuote: false,
|
|
50
|
+
}),
|
|
51
|
+
).toMatchObject({
|
|
52
|
+
canUpgrade: false,
|
|
53
|
+
text: "options delayed fallback",
|
|
54
|
+
tone: "warning",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
resolveOptionsAccessFooterState({
|
|
59
|
+
chain: chain({
|
|
60
|
+
dataSource: "live",
|
|
61
|
+
delayMinutes: 0,
|
|
62
|
+
realtimeEligible: true,
|
|
63
|
+
}),
|
|
64
|
+
clientPlan: "pro",
|
|
65
|
+
hasLiveQuote: false,
|
|
66
|
+
}),
|
|
67
|
+
).toMatchObject({
|
|
68
|
+
canUpgrade: false,
|
|
69
|
+
text: "options delayed fallback",
|
|
70
|
+
tone: "warning",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
resolveOptionsAccessFooterState({
|
|
75
|
+
chain: chain({ realtimeEligible: false }),
|
|
76
|
+
clientPlan: "pro",
|
|
77
|
+
hasLiveQuote: true,
|
|
78
|
+
}).text,
|
|
79
|
+
).toBe("real-time options");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("makes the current plan authoritative over cached chain metadata", () => {
|
|
83
|
+
expect(
|
|
84
|
+
resolveOptionsAccessFooterState({
|
|
85
|
+
chain: chain({
|
|
86
|
+
dataSource: "live",
|
|
87
|
+
delayMinutes: 0,
|
|
88
|
+
realtimeEligible: true,
|
|
89
|
+
}),
|
|
90
|
+
clientPlan: "free",
|
|
91
|
+
hasLiveQuote: true,
|
|
92
|
+
}),
|
|
93
|
+
).toMatchObject({
|
|
94
|
+
canUpgrade: true,
|
|
95
|
+
text: "15-minute delayed options, upgrade for real-time",
|
|
96
|
+
tone: "warning",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
resolveOptionsAccessFooterState({
|
|
101
|
+
chain: chain({
|
|
102
|
+
dataSource: "delayed",
|
|
103
|
+
delayMinutes: 15,
|
|
104
|
+
realtimeEligible: false,
|
|
105
|
+
}),
|
|
106
|
+
clientPlan: "pro",
|
|
107
|
+
hasLiveQuote: true,
|
|
108
|
+
}),
|
|
109
|
+
).toMatchObject({
|
|
110
|
+
canUpgrade: false,
|
|
111
|
+
text: "real-time options",
|
|
112
|
+
tone: "positive",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import type { PaneFooterSegment } from "../../../components";
|
|
3
|
+
import { apiClient } from "../../../api-client";
|
|
4
|
+
import { t, tf } from "../../../i18n";
|
|
5
|
+
import { useShortcut } from "../../../react/input";
|
|
6
|
+
import type { OptionsChain } from "../../../types/financials";
|
|
7
|
+
import { useRendererHost } from "../../../ui";
|
|
8
|
+
import { CLOUD_UPGRADE_URL } from "../shared/cloud-upgrade";
|
|
9
|
+
import { usePaneStatusFooter } from "../shared/pane-footer";
|
|
10
|
+
|
|
11
|
+
export interface OptionsAccessFooterState {
|
|
12
|
+
canUpgrade: boolean;
|
|
13
|
+
text: string;
|
|
14
|
+
tone: "positive" | "warning";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveOptionsAccessFooterState({
|
|
18
|
+
chain,
|
|
19
|
+
clientPlan,
|
|
20
|
+
hasLiveQuote,
|
|
21
|
+
}: {
|
|
22
|
+
chain: OptionsChain | null | undefined;
|
|
23
|
+
clientPlan: "free" | "pro" | null;
|
|
24
|
+
hasLiveQuote: boolean;
|
|
25
|
+
}): OptionsAccessFooterState {
|
|
26
|
+
if (clientPlan !== "pro") {
|
|
27
|
+
const delayMinutes = chain?.delayMinutes && chain.delayMinutes > 0 ? chain.delayMinutes : 15;
|
|
28
|
+
return {
|
|
29
|
+
canUpgrade: true,
|
|
30
|
+
text: tf("{count}-minute delayed options, upgrade for real-time", {
|
|
31
|
+
count: delayMinutes,
|
|
32
|
+
}),
|
|
33
|
+
tone: "warning",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (hasLiveQuote) {
|
|
38
|
+
return {
|
|
39
|
+
canUpgrade: false,
|
|
40
|
+
text: t("real-time options"),
|
|
41
|
+
tone: "positive",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
canUpgrade: false,
|
|
47
|
+
text: t("options delayed fallback"),
|
|
48
|
+
tone: "warning",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildOptionsAccessFooterSegment(
|
|
53
|
+
state: OptionsAccessFooterState,
|
|
54
|
+
openUpgrade: () => void,
|
|
55
|
+
): PaneFooterSegment {
|
|
56
|
+
return {
|
|
57
|
+
id: "options-access",
|
|
58
|
+
...(state.canUpgrade ? { onPress: openUpgrade } : {}),
|
|
59
|
+
parts: [{ text: state.text, tone: state.tone }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useOptionsAccessFooter({
|
|
64
|
+
chain,
|
|
65
|
+
error,
|
|
66
|
+
focused,
|
|
67
|
+
hasLiveQuote,
|
|
68
|
+
loading,
|
|
69
|
+
}: {
|
|
70
|
+
chain: OptionsChain | null | undefined;
|
|
71
|
+
error?: string | null;
|
|
72
|
+
focused: boolean;
|
|
73
|
+
hasLiveQuote: boolean;
|
|
74
|
+
loading?: boolean;
|
|
75
|
+
}): OptionsAccessFooterState {
|
|
76
|
+
const rendererHost = useRendererHost();
|
|
77
|
+
const user = apiClient.getCurrentUser();
|
|
78
|
+
const clientPlan = user?.emailVerified === true && user.plan === "pro" ? "pro" : "free";
|
|
79
|
+
const state = resolveOptionsAccessFooterState({
|
|
80
|
+
chain,
|
|
81
|
+
clientPlan,
|
|
82
|
+
hasLiveQuote,
|
|
83
|
+
});
|
|
84
|
+
const openUpgrade = useCallback(() => {
|
|
85
|
+
void rendererHost.openExternal(CLOUD_UPGRADE_URL);
|
|
86
|
+
}, [rendererHost]);
|
|
87
|
+
|
|
88
|
+
useShortcut(
|
|
89
|
+
(event) => {
|
|
90
|
+
const key = (event.name ?? event.key ?? "").toLowerCase();
|
|
91
|
+
if (!focused || !state.canUpgrade || key !== "u") return;
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
openUpgrade();
|
|
95
|
+
},
|
|
96
|
+
{ scope: "options:upgrade" },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const info = useMemo(
|
|
100
|
+
() => [buildOptionsAccessFooterSegment(state, openUpgrade)],
|
|
101
|
+
[openUpgrade, state.canUpgrade, state.text, state.tone],
|
|
102
|
+
);
|
|
103
|
+
usePaneStatusFooter({
|
|
104
|
+
registrationId: "options",
|
|
105
|
+
loading,
|
|
106
|
+
error,
|
|
107
|
+
info,
|
|
108
|
+
});
|
|
109
|
+
return state;
|
|
110
|
+
}
|