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
|
@@ -217,12 +217,6 @@ export const chartComposerModule: PluginModule = {
|
|
|
217
217
|
},
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
-
export { CHART_COMPOSER_PANE_ID } from "../../../types/config";
|
|
221
|
-
export { ChartComposerPane, ChartComposerResearchTab, ChartComposerSurface } from "./pane";
|
|
222
|
-
export { DateWindowDialog } from "./date-window-dialog";
|
|
223
|
-
export { SeriesEditorDialog } from "./editor";
|
|
224
220
|
export * from "./chart-spec";
|
|
225
221
|
export * from "./presets";
|
|
226
222
|
export * from "./settings";
|
|
227
|
-
|
|
228
|
-
export default chartComposerModule;
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
buildPriceChartPreset,
|
|
42
42
|
applySeriesStyle,
|
|
43
43
|
chartSeriesLabel,
|
|
44
|
+
defaultFinancialTimestampMode,
|
|
44
45
|
getSelectedBuiltinStudies,
|
|
45
46
|
getSelectedPairStudies,
|
|
46
47
|
setBuiltinStudies,
|
|
@@ -66,7 +67,12 @@ const RESOLUTION_TABS_WIDTH = RESOLUTION_TABS.reduce(
|
|
|
66
67
|
(width, tab) => width + [...tab.label].length + 1,
|
|
67
68
|
0,
|
|
68
69
|
);
|
|
69
|
-
const AUTO_VIEWPORT_DEBOUNCE_MS =
|
|
70
|
+
const AUTO_VIEWPORT_DEBOUNCE_MS = 350;
|
|
71
|
+
|
|
72
|
+
interface RuntimeChartViewport {
|
|
73
|
+
start: Date;
|
|
74
|
+
end: Date;
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
function footerAnchorPoint(event?: PaneFooterPressEvent): { x: number; y: number } | undefined {
|
|
72
78
|
const x = event?.pixelX;
|
|
@@ -76,7 +82,7 @@ function footerAnchorPoint(event?: PaneFooterPressEvent): { x: number; y: number
|
|
|
76
82
|
: undefined;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
interface ChartComposerSurfaceProps {
|
|
80
86
|
spec: ChartSpec;
|
|
81
87
|
setSpec: (next: ChartSpec) => void;
|
|
82
88
|
focused: boolean;
|
|
@@ -93,7 +99,7 @@ function isPriceStudyTarget(spec: ChartSpec): boolean {
|
|
|
93
99
|
));
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
function ChartComposerSurface({
|
|
97
103
|
spec,
|
|
98
104
|
setSpec,
|
|
99
105
|
focused,
|
|
@@ -110,6 +116,7 @@ export function ChartComposerSurface({
|
|
|
110
116
|
range: spec.viewport.range,
|
|
111
117
|
resolution: spec.viewport.resolution,
|
|
112
118
|
dateWindow: spec.viewport.dateWindow ?? null,
|
|
119
|
+
maxPoints: spec.viewport.maxPoints ?? null,
|
|
113
120
|
sources: spec.series.map((entry) => entry.source.kind === "security"
|
|
114
121
|
? [
|
|
115
122
|
entry.id,
|
|
@@ -118,20 +125,31 @@ export function ChartComposerSurface({
|
|
|
118
125
|
entry.source.instrument.exchange ?? "",
|
|
119
126
|
entry.source.fieldId,
|
|
120
127
|
entry.source.period ?? "auto",
|
|
128
|
+
entry.source.timestampMode
|
|
129
|
+
?? defaultFinancialTimestampMode(entry.source.fieldId)
|
|
130
|
+
?? "",
|
|
121
131
|
]
|
|
122
132
|
: [entry.id, entry.source.kind, entry.source.seriesId]),
|
|
123
|
-
}), [spec.series, spec.viewport.dateWindow, spec.viewport.range, spec.viewport.resolution]);
|
|
124
|
-
const [
|
|
133
|
+
}), [spec.series, spec.viewport.dateWindow, spec.viewport.maxPoints, spec.viewport.range, spec.viewport.resolution]);
|
|
134
|
+
const [runtimeViewportState, setRuntimeViewportState] = useState<{
|
|
125
135
|
key: string;
|
|
126
|
-
|
|
136
|
+
adaptiveViewport: RuntimeChartViewport | null;
|
|
137
|
+
requestViewport: RuntimeChartViewport;
|
|
127
138
|
} | null>(null);
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
const runtimeViewportTimerRef = useRef<ReturnType<typeof globalThis.setTimeout> | null>(null);
|
|
140
|
+
const adaptiveViewportRef = useRef<RuntimeChartViewport | null>(null);
|
|
141
|
+
const requestViewportRef = useRef<RuntimeChartViewport | null>(null);
|
|
142
|
+
const activeRuntimeViewport = runtimeViewportState?.key === authoredViewportKey
|
|
143
|
+
? runtimeViewportState
|
|
132
144
|
: null;
|
|
133
145
|
const targetPointCount = Math.max(60, Math.floor(width * 1.25));
|
|
134
|
-
const resolution = useResolvedChartSpec(spec, {
|
|
146
|
+
const resolution = useResolvedChartSpec(spec, {
|
|
147
|
+
autoViewport: spec.viewport.resolution === "auto"
|
|
148
|
+
? activeRuntimeViewport?.adaptiveViewport
|
|
149
|
+
: null,
|
|
150
|
+
requestViewport: activeRuntimeViewport?.requestViewport,
|
|
151
|
+
targetPointCount,
|
|
152
|
+
});
|
|
135
153
|
const selectedStudies = getSelectedBuiltinStudies(spec);
|
|
136
154
|
const selectedPairStudies = getSelectedPairStudies(spec);
|
|
137
155
|
const inlineStyleTarget = useMemo(() => getChartInlineStyleTarget(spec), [spec]);
|
|
@@ -184,42 +202,55 @@ export function ChartComposerSurface({
|
|
|
184
202
|
if (!focused) dispatch({ type: "FOCUS_PANE", paneId });
|
|
185
203
|
}, [dispatch, focused, paneId]);
|
|
186
204
|
useEffect(() => {
|
|
187
|
-
if (
|
|
188
|
-
clearTimeout(
|
|
189
|
-
|
|
205
|
+
if (runtimeViewportTimerRef.current !== null) {
|
|
206
|
+
clearTimeout(runtimeViewportTimerRef.current);
|
|
207
|
+
runtimeViewportTimerRef.current = null;
|
|
190
208
|
}
|
|
191
|
-
|
|
209
|
+
adaptiveViewportRef.current = null;
|
|
210
|
+
requestViewportRef.current = null;
|
|
211
|
+
setRuntimeViewportState((current) => current?.key === authoredViewportKey ? current : null);
|
|
192
212
|
return () => {
|
|
193
|
-
if (
|
|
194
|
-
clearTimeout(
|
|
195
|
-
|
|
213
|
+
if (runtimeViewportTimerRef.current !== null) {
|
|
214
|
+
clearTimeout(runtimeViewportTimerRef.current);
|
|
215
|
+
runtimeViewportTimerRef.current = null;
|
|
196
216
|
}
|
|
197
217
|
};
|
|
198
218
|
}, [authoredViewportKey]);
|
|
199
|
-
const handleChartViewportChange = useCallback((
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
const handleChartViewportChange = useCallback((
|
|
220
|
+
next: { start: Date; end: Date } | null,
|
|
221
|
+
interaction: "pan" | "reset" | "sync" | "zoom",
|
|
222
|
+
) => {
|
|
223
|
+
if (interaction === "sync") return;
|
|
224
|
+
if (runtimeViewportTimerRef.current !== null) {
|
|
225
|
+
clearTimeout(runtimeViewportTimerRef.current);
|
|
226
|
+
runtimeViewportTimerRef.current = null;
|
|
203
227
|
}
|
|
204
|
-
if (
|
|
205
|
-
|
|
228
|
+
if (!next) {
|
|
229
|
+
adaptiveViewportRef.current = null;
|
|
230
|
+
requestViewportRef.current = null;
|
|
231
|
+
setRuntimeViewportState(null);
|
|
206
232
|
return;
|
|
207
233
|
}
|
|
208
234
|
const start = next.start.getTime();
|
|
209
235
|
const end = next.end.getTime();
|
|
210
236
|
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
const viewport = { start: new Date(start), end: new Date(end) };
|
|
238
|
+
requestViewportRef.current = viewport;
|
|
239
|
+
if (interaction === "zoom" && spec.viewport.resolution === "auto") {
|
|
240
|
+
adaptiveViewportRef.current = viewport;
|
|
241
|
+
}
|
|
242
|
+
runtimeViewportTimerRef.current = globalThis.setTimeout(() => {
|
|
243
|
+
runtimeViewportTimerRef.current = null;
|
|
244
|
+
const requestViewport = requestViewportRef.current;
|
|
245
|
+
if (!requestViewport) return;
|
|
246
|
+
const adaptiveViewport = spec.viewport.resolution === "auto"
|
|
247
|
+
? adaptiveViewportRef.current
|
|
248
|
+
: null;
|
|
249
|
+
setRuntimeViewportState({
|
|
250
|
+
key: authoredViewportKey,
|
|
251
|
+
adaptiveViewport,
|
|
252
|
+
requestViewport,
|
|
253
|
+
});
|
|
223
254
|
}, AUTO_VIEWPORT_DEBOUNCE_MS);
|
|
224
255
|
}, [authoredViewportKey, spec.viewport.resolution]);
|
|
225
256
|
|
|
@@ -564,8 +595,10 @@ export function ChartComposerSurface({
|
|
|
564
595
|
<CompositeChart
|
|
565
596
|
series={plottedSeries}
|
|
566
597
|
legendSeries={resolution.legendSeries}
|
|
598
|
+
timelineSeries={resolution.timelineSeries}
|
|
567
599
|
panels={spec.panels}
|
|
568
600
|
viewport={viewport}
|
|
601
|
+
viewportResetKey={authoredViewportKey}
|
|
569
602
|
width={Math.max(1, width)}
|
|
570
603
|
height={Math.max(4, height - 1)}
|
|
571
604
|
focused={focused}
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../../../utils/exchanges";
|
|
31
31
|
import { MAX_CHART_COMPOSER_SERIES } from "./chart-spec";
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const CHART_FIELD_IDS = {
|
|
34
34
|
price: "market.ohlcv",
|
|
35
35
|
close: "market.close",
|
|
36
36
|
volume: "market.volume",
|
|
@@ -45,8 +45,6 @@ export const CHART_FIELD_IDS = {
|
|
|
45
45
|
evEbitda: "valuation.evEbitda",
|
|
46
46
|
} as const;
|
|
47
47
|
|
|
48
|
-
export type ChartComposerPreset = "G" | "GP" | "GIP" | "CMP" | "GF" | "GE";
|
|
49
|
-
|
|
50
48
|
export type ParsedSeriesExpression =
|
|
51
49
|
| { kind: "security"; symbol: string; exchange?: string; fieldId: string; label?: string }
|
|
52
50
|
| { kind: "economic"; provider: "fred"; seriesId: string; label?: string };
|
|
@@ -647,22 +645,6 @@ export function buildValuationChartPreset(
|
|
|
647
645
|
)));
|
|
648
646
|
}
|
|
649
647
|
|
|
650
|
-
export function buildPresetChartSpec(
|
|
651
|
-
preset: ChartComposerPreset,
|
|
652
|
-
symbols: readonly string[],
|
|
653
|
-
expression = "",
|
|
654
|
-
): ChartSpec {
|
|
655
|
-
const primary = symbols[0] ?? "";
|
|
656
|
-
switch (preset) {
|
|
657
|
-
case "G": return buildCustomChartPreset(expression, primary || null);
|
|
658
|
-
case "GP": return buildPriceChartPreset(primary);
|
|
659
|
-
case "GIP": return buildIntradayPriceChartPreset(primary);
|
|
660
|
-
case "CMP": return buildComparisonChartPreset(symbols);
|
|
661
|
-
case "GF": return buildFundamentalChartPreset(symbols);
|
|
662
|
-
case "GE": return buildValuationChartPreset(symbols);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
648
|
const STUDY_DEFAULTS = {
|
|
667
649
|
volume: { kind: "volume", panelId: "volume", parameters: {} },
|
|
668
650
|
sma20: { kind: "sma", panelId: "main", parameters: { period: 20 } },
|
|
@@ -364,7 +364,7 @@ export function useChatComposerRuntime({
|
|
|
364
364
|
const inputPlaceholder = editingMessage
|
|
365
365
|
? t("Edit message...")
|
|
366
366
|
: replyTo
|
|
367
|
-
? tf("Reply to @{username}...", { username: replyTo.user.username })
|
|
367
|
+
? tf("Reply to @{username}...", { username: replyTo.user.username ?? "unknown" })
|
|
368
368
|
: t("Type a message...");
|
|
369
369
|
|
|
370
370
|
return {
|
|
@@ -23,13 +23,18 @@ export async function fetchLatestChannelMessages(
|
|
|
23
23
|
const hasIncrementalCursor = !!channel.lastCursor;
|
|
24
24
|
const hadMessages = channel.messages.length > 0;
|
|
25
25
|
const countIncrementalUnread = hadMessages && hasIncrementalCursor && !legacyTimestampCursor;
|
|
26
|
+
// The first fetch of a session drops the cursor so anything missed while the
|
|
27
|
+
// socket was connected comes back. Merging is keyed by message id, so
|
|
28
|
+
// re-reading known messages changes nothing and counts no unread.
|
|
29
|
+
const backfillGaps = !channel.backfilled;
|
|
26
30
|
|
|
27
31
|
try {
|
|
28
32
|
const messages = await apiClient.getMessages(channelId, {
|
|
29
33
|
limit: MESSAGE_PAGE_SIZE,
|
|
30
|
-
after: channel.lastCursor ?? undefined,
|
|
34
|
+
after: backfillGaps ? undefined : channel.lastCursor ?? undefined,
|
|
31
35
|
});
|
|
32
|
-
|
|
36
|
+
channel.backfilled = true;
|
|
37
|
+
if ((!hasIncrementalCursor || backfillGaps) && messages.length < MESSAGE_PAGE_SIZE) {
|
|
33
38
|
channel.reachedOldestMessage = true;
|
|
34
39
|
}
|
|
35
40
|
if (messages.length > 0) {
|
|
@@ -51,6 +56,7 @@ export async function fetchLatestChannelMessages(
|
|
|
51
56
|
return;
|
|
52
57
|
} catch {
|
|
53
58
|
const messages = await apiClient.getMessages(channelId, { limit: MESSAGE_PAGE_SIZE });
|
|
59
|
+
channel.backfilled = true;
|
|
54
60
|
if (messages.length < MESSAGE_PAGE_SIZE) {
|
|
55
61
|
channel.reachedOldestMessage = true;
|
|
56
62
|
}
|
|
@@ -21,6 +21,7 @@ export interface ChatSessionUser {
|
|
|
21
21
|
id: string;
|
|
22
22
|
username: string;
|
|
23
23
|
emailVerified: boolean;
|
|
24
|
+
plan?: "free" | "pro";
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function readChatSessionState(
|
|
@@ -40,6 +41,7 @@ export function normalizeSessionUser(user: PersistedAuthUser | null | undefined)
|
|
|
40
41
|
id: user.id,
|
|
41
42
|
username: user.username ?? user.name ?? "account",
|
|
42
43
|
emailVerified: user.emailVerified === true,
|
|
44
|
+
plan: user.plan,
|
|
43
45
|
}
|
|
44
46
|
: null;
|
|
45
47
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { normalizeSessionUser } from "./persistence";
|
|
3
|
+
import { sessionUserFromApiSession } from "./session";
|
|
4
|
+
|
|
5
|
+
describe("chat session market entitlement", () => {
|
|
6
|
+
test("preserves plan through cached and refreshed desktop session mappings", () => {
|
|
7
|
+
expect(
|
|
8
|
+
normalizeSessionUser({
|
|
9
|
+
id: "user-1",
|
|
10
|
+
emailVerified: true,
|
|
11
|
+
username: "vince",
|
|
12
|
+
plan: "pro",
|
|
13
|
+
}),
|
|
14
|
+
).toMatchObject({ id: "user-1", plan: "pro" });
|
|
15
|
+
|
|
16
|
+
expect(
|
|
17
|
+
sessionUserFromApiSession({
|
|
18
|
+
id: "user-1",
|
|
19
|
+
emailVerified: true,
|
|
20
|
+
username: "vince",
|
|
21
|
+
plan: "free",
|
|
22
|
+
}),
|
|
23
|
+
).toMatchObject({ id: "user-1", plan: "free" });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -8,6 +8,7 @@ export type ChatApiSession = {
|
|
|
8
8
|
username?: string | null;
|
|
9
9
|
name?: string | null;
|
|
10
10
|
emailVerified?: boolean | null;
|
|
11
|
+
plan?: "free" | "pro";
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export function getSessionIdentity(user: ChatSessionUser | null): string {
|
|
@@ -19,6 +20,7 @@ export function sessionUserFromApiSession(session: ChatApiSession): ChatSessionU
|
|
|
19
20
|
id: session.id,
|
|
20
21
|
username: session.username ?? session.name ?? "account",
|
|
21
22
|
emailVerified: !!session.emailVerified,
|
|
23
|
+
plan: session.plan,
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -73,6 +73,12 @@ export interface ChannelRuntimeState {
|
|
|
73
73
|
refreshMessagesPromise: Promise<void> | null;
|
|
74
74
|
loadOlderMessagesPromise: Promise<void> | null;
|
|
75
75
|
reachedOldestMessage: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* False until this session has fetched a full latest page. The incremental
|
|
78
|
+
* cursor only ever asks for newer messages, so a message missed live can
|
|
79
|
+
* never be requested again; one uncursored fetch per session repairs it.
|
|
80
|
+
*/
|
|
81
|
+
backfilled: boolean;
|
|
76
82
|
messages: ChatMessage[];
|
|
77
83
|
pendingMessages: ChatMessage[];
|
|
78
84
|
draft: string;
|
|
@@ -105,6 +111,7 @@ export function createEmptyChannelState(): ChannelRuntimeState {
|
|
|
105
111
|
refreshMessagesPromise: null,
|
|
106
112
|
loadOlderMessagesPromise: null,
|
|
107
113
|
reachedOldestMessage: false,
|
|
114
|
+
backfilled: false,
|
|
108
115
|
messages: [],
|
|
109
116
|
pendingMessages: [],
|
|
110
117
|
draft: "",
|
|
@@ -501,19 +501,76 @@ describe("ChatController", () => {
|
|
|
501
501
|
const calls: Array<{ channelId: string; opts?: { after?: string; before?: string; limit?: number } }> = [];
|
|
502
502
|
apiClient.getMessages = async (channelId, opts) => {
|
|
503
503
|
calls.push({ channelId, opts });
|
|
504
|
-
|
|
504
|
+
if (!opts?.after) return [initial, next];
|
|
505
|
+
return opts.after === "m1" ? [next] : [];
|
|
505
506
|
};
|
|
506
507
|
|
|
507
508
|
await controller.refreshMessages();
|
|
508
509
|
|
|
509
|
-
expect(calls).
|
|
510
|
-
channelId: "everyone",
|
|
511
|
-
opts: { limit: 50, after: "m1" },
|
|
512
|
-
}]);
|
|
510
|
+
expect(calls[0]?.opts?.after).toBeUndefined();
|
|
513
511
|
expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2"]);
|
|
514
512
|
expect(persistence.getState<{ lastCursor: string }>("channel:everyone", { schemaVersion: 1 })).toMatchObject({
|
|
515
513
|
lastCursor: "m2",
|
|
516
514
|
});
|
|
515
|
+
|
|
516
|
+
await controller.refreshMessages();
|
|
517
|
+
|
|
518
|
+
expect(calls[1]?.opts?.after).toBe("m2");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("recovers a message the cursor already moved past", async () => {
|
|
522
|
+
const persistence = new MemoryPersistence();
|
|
523
|
+
const controller = new ChatController();
|
|
524
|
+
const asked: ChatMessage = {
|
|
525
|
+
id: "m1",
|
|
526
|
+
channelId: "everyone",
|
|
527
|
+
content: "why is the chart glitching",
|
|
528
|
+
replyToId: null,
|
|
529
|
+
createdAt: "2026-03-28T00:00:00.000Z",
|
|
530
|
+
user: { id: "u1", username: "vince", displayName: "Vince" },
|
|
531
|
+
};
|
|
532
|
+
const missed: ChatMessage = {
|
|
533
|
+
id: "m2",
|
|
534
|
+
channelId: "everyone",
|
|
535
|
+
content: "charts open with GP",
|
|
536
|
+
replyToId: "m1",
|
|
537
|
+
createdAt: "2026-03-28T00:01:00.000Z",
|
|
538
|
+
user: { id: "u2", username: "gloombot", displayName: "Gloombot" },
|
|
539
|
+
};
|
|
540
|
+
const acknowledged: ChatMessage = {
|
|
541
|
+
id: "m3",
|
|
542
|
+
channelId: "everyone",
|
|
543
|
+
content: "got it",
|
|
544
|
+
replyToId: null,
|
|
545
|
+
createdAt: "2026-03-28T00:02:00.000Z",
|
|
546
|
+
user: { id: "u1", username: "vince", displayName: "Vince" },
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// The cursor already sits on m3, so an incremental fetch can never ask for
|
|
550
|
+
// m2 again even though the server still has it.
|
|
551
|
+
persistence.setState("channel:everyone", {
|
|
552
|
+
draft: "",
|
|
553
|
+
replyToId: null,
|
|
554
|
+
lastCursor: "m3",
|
|
555
|
+
lastViewedMessageId: "m3",
|
|
556
|
+
}, { schemaVersion: 1 });
|
|
557
|
+
persistence.setResource(TRANSCRIPT_KIND, TRANSCRIPT_KEY, {
|
|
558
|
+
messages: [asked, acknowledged],
|
|
559
|
+
}, {
|
|
560
|
+
sourceKey: TRANSCRIPT_SOURCE,
|
|
561
|
+
schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
|
|
562
|
+
cachePolicy: { staleMs: 1_000, expireMs: 2_000 },
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
controller.attachPersistence(persistence);
|
|
566
|
+
|
|
567
|
+
apiClient.getMessages = async (_channelId, opts) => (
|
|
568
|
+
opts?.after ? [] : [asked, missed, acknowledged]
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
await controller.refreshMessages();
|
|
572
|
+
|
|
573
|
+
expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2", "m3"]);
|
|
517
574
|
});
|
|
518
575
|
|
|
519
576
|
test("hydrates missing transcript cache without marking history unread", async () => {
|
|
@@ -603,19 +660,21 @@ describe("ChatController", () => {
|
|
|
603
660
|
const calls: Array<{ channelId: string; opts?: { after?: string; before?: string; limit?: number } }> = [];
|
|
604
661
|
apiClient.getMessages = async (channelId, opts) => {
|
|
605
662
|
calls.push({ channelId, opts });
|
|
606
|
-
return opts?.after
|
|
663
|
+
return opts?.after ? [] : [cached, fresh];
|
|
607
664
|
};
|
|
608
665
|
|
|
609
666
|
await controller.refreshMessages();
|
|
610
667
|
|
|
611
|
-
expect(calls).
|
|
612
|
-
channelId: "everyone",
|
|
613
|
-
opts: { limit: 50, after: "m1" },
|
|
614
|
-
}]);
|
|
668
|
+
expect(calls[0]?.opts?.after).toBeUndefined();
|
|
615
669
|
expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2"]);
|
|
616
670
|
expect(persistence.getState<{ lastCursor: string }>("channel:everyone", { schemaVersion: 1 })).toMatchObject({
|
|
617
671
|
lastCursor: "m2",
|
|
618
672
|
});
|
|
673
|
+
|
|
674
|
+
await controller.refreshMessages();
|
|
675
|
+
|
|
676
|
+
// The stale m99 cursor never reaches the server.
|
|
677
|
+
expect(calls[1]?.opts?.after).toBe("m2");
|
|
619
678
|
});
|
|
620
679
|
|
|
621
680
|
test("shows a pending message immediately and replaces it when the send succeeds", async () => {
|
|
@@ -1398,16 +1457,10 @@ describe("ChatController", () => {
|
|
|
1398
1457
|
|
|
1399
1458
|
await controller.refreshMessages();
|
|
1400
1459
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
},
|
|
1406
|
-
{
|
|
1407
|
-
channelId: "everyone",
|
|
1408
|
-
opts: { limit: 50 },
|
|
1409
|
-
},
|
|
1410
|
-
]);
|
|
1460
|
+
// The session backfill already ignores the cursor, so the legacy timestamp
|
|
1461
|
+
// is never sent and the old wasted round trip is gone.
|
|
1462
|
+
expect(calls).toHaveLength(1);
|
|
1463
|
+
expect(calls[0]?.opts?.after).toBeUndefined();
|
|
1411
1464
|
expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2"]);
|
|
1412
1465
|
expect(persistence.getState<{ lastCursor: string }>("channel:everyone", { schemaVersion: 1 })).toMatchObject({
|
|
1413
1466
|
lastCursor: "m2",
|
|
@@ -2,6 +2,7 @@ import { memo } from "react";
|
|
|
2
2
|
import { Box, Span, Text } from "../../../../ui";
|
|
3
3
|
import { hoverBg } from "../../../../theme/colors";
|
|
4
4
|
import { t } from "../../../../i18n";
|
|
5
|
+
import { useAppLanguage } from "../../../../i18n/react";
|
|
5
6
|
import { MESSAGE_ACTION_WIDTH, normalizeInlinePreview } from "../layout";
|
|
6
7
|
import { ChatActionChip } from "./action-chip";
|
|
7
8
|
import { ResponsiveTickerBadgeText } from "./inline-tokens";
|
|
@@ -30,6 +31,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
30
31
|
}: ChatMessageBaseProps & {
|
|
31
32
|
registerMessageElement: (messageId: string, node: unknown | null) => void;
|
|
32
33
|
}) {
|
|
34
|
+
useAppLanguage();
|
|
33
35
|
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
34
36
|
const canEditMessage = msg.id === latestEditableMessageId;
|
|
35
37
|
const showInlineReplyAction = !state.grouped && canSend;
|
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { PaneFooterSegment } from "../../../components";
|
|
2
3
|
import { formatTimeAgo } from "../../../utils/format";
|
|
3
4
|
import {
|
|
4
5
|
TWITTER_FEED_PANE_ID,
|
|
5
6
|
type TwitterFeed,
|
|
6
7
|
} from "./model";
|
|
8
|
+
import { usePaneStatusFooter } from "../shared/pane-footer";
|
|
7
9
|
|
|
8
10
|
export function useTwitterFeedFooter({
|
|
9
11
|
activeFeed,
|
|
10
|
-
addFeed,
|
|
11
|
-
blurSearch,
|
|
12
|
-
focusSearch,
|
|
13
|
-
removeFeed,
|
|
14
|
-
searchFocused,
|
|
15
12
|
}: {
|
|
16
13
|
activeFeed: TwitterFeed | null;
|
|
17
|
-
addFeed: () => void;
|
|
18
|
-
blurSearch: () => void;
|
|
19
|
-
focusSearch: () => void;
|
|
20
|
-
removeFeed: (feedId: string) => void;
|
|
21
|
-
searchFocused: boolean;
|
|
22
14
|
}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
? [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
: [],
|
|
30
|
-
hints: searchFocused
|
|
31
|
-
? [
|
|
32
|
-
{ id: "done", key: "Esc", label: "done", onPress: blurSearch },
|
|
33
|
-
]
|
|
34
|
-
: [
|
|
35
|
-
{ id: "new", key: "n", label: "ew", onPress: () => addFeed() },
|
|
36
|
-
{ id: "search", key: "/", label: "search", onPress: focusSearch, disabled: !activeFeed },
|
|
37
|
-
{ id: "delete", key: "d", label: "elete", onPress: activeFeed ? () => removeFeed(activeFeed.id) : undefined, disabled: !activeFeed },
|
|
38
|
-
],
|
|
39
|
-
}), [activeFeed, addFeed, blurSearch, focusSearch, removeFeed, searchFocused]);
|
|
15
|
+
const info = useMemo<PaneFooterSegment[]>(() => (
|
|
16
|
+
activeFeed?.lastSuccessAt
|
|
17
|
+
? [{ id: "last", parts: [{ text: `ran ${formatTimeAgo(new Date(activeFeed.lastSuccessAt))}`, tone: "muted" }] }]
|
|
18
|
+
: []
|
|
19
|
+
), [activeFeed?.lastSuccessAt]);
|
|
20
|
+
usePaneStatusFooter({ registrationId: TWITTER_FEED_PANE_ID, info });
|
|
40
21
|
}
|
|
@@ -249,11 +249,6 @@ export function TwitterFeedPane({ focused, width, height }: PaneProps) {
|
|
|
249
249
|
|
|
250
250
|
useTwitterFeedFooter({
|
|
251
251
|
activeFeed,
|
|
252
|
-
addFeed,
|
|
253
|
-
blurSearch,
|
|
254
|
-
focusSearch,
|
|
255
|
-
removeFeed,
|
|
256
|
-
searchFocused,
|
|
257
252
|
});
|
|
258
253
|
|
|
259
254
|
const searchBar = activeFeed ? (
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import { Box, ScrollBox, Text, TextAttributes
|
|
2
|
+
import { Box, ScrollBox, Text, TextAttributes } from "../../../ui";
|
|
3
3
|
import {
|
|
4
4
|
DataTableStackView,
|
|
5
5
|
TickerBadgeList,
|
|
6
|
-
usePaneFooter,
|
|
7
6
|
type DataTableCell,
|
|
8
7
|
type DataTableKeyEvent,
|
|
9
8
|
type DataTableRootKeyContext,
|
|
@@ -34,6 +33,7 @@ import {
|
|
|
34
33
|
type TweetSortColumnId,
|
|
35
34
|
type TweetSortDirection,
|
|
36
35
|
} from "./model";
|
|
36
|
+
import { usePaneStatusLinkFooter } from "../shared/pane-footer";
|
|
37
37
|
|
|
38
38
|
function isAuthError(error: string | null): boolean {
|
|
39
39
|
return !!error && /unauthorized|verification/i.test(error);
|
|
@@ -172,7 +172,6 @@ export function TweetSearchTable({
|
|
|
172
172
|
emptyStateTitle?: string;
|
|
173
173
|
emptyStateHint?: string;
|
|
174
174
|
}) {
|
|
175
|
-
const rendererHost = useRendererHost();
|
|
176
175
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
177
176
|
const { data, loading, error, reload } = useTweetSearchData(requestKey, load, onResult, onError, enabled);
|
|
178
177
|
const [selectedTweetId, setSelectedTweetId] = useState<string | null>(null);
|
|
@@ -186,6 +185,16 @@ export function TweetSearchTable({
|
|
|
186
185
|
const selectedIndex = rows.findIndex((tweet) => tweet.id === selectedTweetId);
|
|
187
186
|
const activeIndex = selectedIndex >= 0 ? selectedIndex : rows.length > 0 ? 0 : -1;
|
|
188
187
|
const selectedTweet = rows[activeIndex] ?? null;
|
|
188
|
+
const openSelectedTweet = usePaneStatusLinkFooter({
|
|
189
|
+
registrationId: footerId,
|
|
190
|
+
focused,
|
|
191
|
+
url: detailOpen ? selectedTweet?.url : null,
|
|
192
|
+
source: detailOpen && selectedTweet
|
|
193
|
+
? `@${selectedTweet.author.userName || selectedTweet.author.name}`
|
|
194
|
+
: null,
|
|
195
|
+
loading,
|
|
196
|
+
error,
|
|
197
|
+
});
|
|
189
198
|
|
|
190
199
|
useEffect(() => {
|
|
191
200
|
if (rows.length === 0) {
|
|
@@ -198,9 +207,6 @@ export function TweetSearchTable({
|
|
|
198
207
|
}
|
|
199
208
|
}, [rows, selectedIndex, selectedTweetId]);
|
|
200
209
|
|
|
201
|
-
const openSelectedTweet = useCallback(() => {
|
|
202
|
-
if (selectedTweet?.url) void rendererHost.openExternal(selectedTweet.url);
|
|
203
|
-
}, [rendererHost, selectedTweet]);
|
|
204
210
|
const openUsernameFeed = useCallback((username: string) => {
|
|
205
211
|
const normalizedUsername = normalizeTwitterUsername(username);
|
|
206
212
|
if (!normalizedUsername) return;
|
|
@@ -247,17 +253,6 @@ export function TweetSearchTable({
|
|
|
247
253
|
return true;
|
|
248
254
|
}, [openSelectedTweet]);
|
|
249
255
|
|
|
250
|
-
usePaneFooter(footerId, () => ({
|
|
251
|
-
info: [
|
|
252
|
-
...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
|
|
253
|
-
...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
254
|
-
],
|
|
255
|
-
hints: [
|
|
256
|
-
{ id: "refresh", key: "r", label: "efresh", onPress: reload },
|
|
257
|
-
...(detailOpen ? [{ id: "open", key: "o", label: "pen", onPress: openSelectedTweet, disabled: !selectedTweet?.url }] : []),
|
|
258
|
-
],
|
|
259
|
-
}), [detailOpen, error, footerId, loading, openSelectedTweet, reload, selectedTweet?.url]);
|
|
260
|
-
|
|
261
256
|
const renderCell = useCallback((
|
|
262
257
|
tweet: CloudTweetPayload,
|
|
263
258
|
column: TweetColumn,
|