gloomberb 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -5,7 +5,7 @@ import { useArticleSummary, useResolvedEntryValue } from "../../../market-data/h
|
|
|
5
5
|
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
6
6
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../runtime";
|
|
7
7
|
import { EmptyState } from "../../../components";
|
|
8
|
-
import { useLoadNewsStory, useNewsArticles } from "../../../news/hooks";
|
|
8
|
+
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../news/hooks";
|
|
9
9
|
import { newsWireModule } from "./wire";
|
|
10
10
|
import { NewsDetailView, useNewsArticleDetail } from "./wire/news/detail-view";
|
|
11
11
|
import {
|
|
@@ -33,18 +33,20 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
|
|
|
33
33
|
DEFAULT_SORT,
|
|
34
34
|
);
|
|
35
35
|
const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
|
|
36
|
-
const
|
|
37
|
-
feed: "ticker",
|
|
36
|
+
const newsQuery = instrument ? {
|
|
37
|
+
feed: "ticker" as const,
|
|
38
38
|
ticker: instrument.symbol,
|
|
39
39
|
exchange: instrument.exchange,
|
|
40
|
-
tickerTier: "primary",
|
|
40
|
+
tickerTier: "primary" as const,
|
|
41
41
|
limit: NEWS_ITEM_LIMIT,
|
|
42
|
-
} : null
|
|
42
|
+
} : null;
|
|
43
|
+
const newsState = useNewsArticles(newsQuery);
|
|
43
44
|
const news = usePersistedNewsArticles(
|
|
44
45
|
`articles:${instrument?.symbol ?? "none"}:${instrument?.exchange ?? ""}`,
|
|
45
46
|
newsState.articles,
|
|
46
47
|
);
|
|
47
48
|
const { readArticleIds, markArticleRead } = useNewsReadState();
|
|
49
|
+
const { scrollRef, onBodyScrollActivity } = useNewsTableLoadMore(newsQuery, newsState);
|
|
48
50
|
const loadNewsStory = useLoadNewsStory();
|
|
49
51
|
const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(news, loadNewsStory);
|
|
50
52
|
const loading = newsState.phase === "loading"
|
|
@@ -118,11 +120,13 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
|
|
|
118
120
|
})}
|
|
119
121
|
emptyStateTitle={`No news for ${ticker.metadata.ticker}`}
|
|
120
122
|
emptyStateHint="Stories appear as sources publish them."
|
|
123
|
+
scrollRef={scrollRef}
|
|
124
|
+
onBodyScrollActivity={onBodyScrollActivity}
|
|
121
125
|
/>
|
|
122
126
|
);
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
const tickerNewsModule: PluginModule = {
|
|
129
|
+
export const tickerNewsModule: PluginModule = {
|
|
126
130
|
panes: [
|
|
127
131
|
{
|
|
128
132
|
id: "ticker-news",
|
|
@@ -143,6 +147,7 @@ const tickerNewsModule: PluginModule = {
|
|
|
143
147
|
description: "Company news for the selected ticker.",
|
|
144
148
|
keywords: ["company", "ticker", "news", "headlines", "cn"],
|
|
145
149
|
shortcut: "CN",
|
|
150
|
+
publicShare: true,
|
|
146
151
|
}),
|
|
147
152
|
],
|
|
148
153
|
|
|
@@ -52,6 +52,8 @@ function createReadyNewsService(articles: NewsArticle[]): { service: NewsService
|
|
|
52
52
|
error: null,
|
|
53
53
|
updatedAt: Date.now(),
|
|
54
54
|
sourceIds: ["test"],
|
|
55
|
+
nextCursor: null,
|
|
56
|
+
loadingMore: false,
|
|
55
57
|
};
|
|
56
58
|
const listeners = new Set<(state: NewsQueryState) => void>();
|
|
57
59
|
let queryStateCalls = 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box } from "../../../../../ui";
|
|
2
2
|
import type { PaneProps } from "../../../../../types/plugin";
|
|
3
|
-
import { useLoadNewsStory, useNewsArticles } from "../../../../../news/hooks";
|
|
3
|
+
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
|
|
4
4
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../../../runtime";
|
|
5
5
|
import { NewsDetailView, useNewsArticleDetail } from "../news/detail-view";
|
|
6
6
|
import {
|
|
@@ -18,6 +18,7 @@ const DEFAULT_SORT: NewsSortPreference = { columnId: "importance", direction: "d
|
|
|
18
18
|
export function BreakingPane({ focused, width, height }: PaneProps) {
|
|
19
19
|
const breakingState = useNewsArticles(NEWS_QUERY_PRESETS.breaking);
|
|
20
20
|
const articles = usePersistedNewsArticles("breaking:articles", breakingState.articles);
|
|
21
|
+
const { scrollRef, onBodyScrollActivity } = useNewsTableLoadMore(NEWS_QUERY_PRESETS.breaking, breakingState);
|
|
21
22
|
const loading = breakingState.phase === "loading"
|
|
22
23
|
|| (breakingState.phase === "refreshing" && articles.length === 0);
|
|
23
24
|
const error = breakingState.error;
|
|
@@ -73,6 +74,8 @@ export function BreakingPane({ focused, width, height }: PaneProps) {
|
|
|
73
74
|
})}
|
|
74
75
|
emptyStateTitle="No breaking news"
|
|
75
76
|
emptyStateHint="Breaking stories appear when high-priority headlines arrive."
|
|
77
|
+
scrollRef={scrollRef}
|
|
78
|
+
onBodyScrollActivity={onBodyScrollActivity}
|
|
76
79
|
/>
|
|
77
80
|
);
|
|
78
81
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getEnabledNewsFeeds,
|
|
7
7
|
loadNewsFeedSettings,
|
|
8
8
|
removeUserNewsFeed,
|
|
9
|
+
saveNewsFeedSettings,
|
|
9
10
|
setDefaultNewsFeedEnabled,
|
|
10
11
|
updateUserNewsFeed,
|
|
11
12
|
} from "./feed-config";
|
|
@@ -31,7 +32,7 @@ class MemoryConfigState implements PluginConfigState {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
describe("news feed config", () => {
|
|
34
|
-
test("normalizes legacy JSON feed storage and migrates disabled default feed names to ids", () => {
|
|
35
|
+
test("normalizes legacy JSON feed storage and migrates disabled default feed names to ids", async () => {
|
|
35
36
|
const config = new MemoryConfigState();
|
|
36
37
|
config.values.set("feeds", JSON.stringify([
|
|
37
38
|
{ url: "https://example.com/rss.xml", name: "Example", authority: 120 },
|
|
@@ -41,10 +42,23 @@ describe("news feed config", () => {
|
|
|
41
42
|
|
|
42
43
|
const settings = loadNewsFeedSettings(config);
|
|
43
44
|
|
|
45
|
+
expect(settings.needsMigration).toBe(true);
|
|
44
46
|
expect(settings.userFeeds).toHaveLength(1);
|
|
45
47
|
expect(settings.userFeeds[0]!.id).toMatch(/^user-/);
|
|
46
48
|
expect(settings.userFeeds[0]!.authority).toBe(100);
|
|
47
49
|
expect(settings.disabledDefaultFeedIds).toEqual(["default-cnbc-top"]);
|
|
50
|
+
|
|
51
|
+
await saveNewsFeedSettings(config, settings);
|
|
52
|
+
expect(loadNewsFeedSettings(config).needsMigration).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("does not rewrite canonical feed settings", async () => {
|
|
56
|
+
const config = new MemoryConfigState();
|
|
57
|
+
await addUserNewsFeed(config, {
|
|
58
|
+
url: "https://example.com/feed",
|
|
59
|
+
name: "Example",
|
|
60
|
+
});
|
|
61
|
+
expect(loadNewsFeedSettings(config).needsMigration).toBe(false);
|
|
48
62
|
});
|
|
49
63
|
|
|
50
64
|
test("adds, updates, and removes user feeds through typed helpers", async () => {
|
|
@@ -12,7 +12,7 @@ const DEFAULT_FEED_IDS = new Set(DEFAULT_FEEDS.map((feed) => feed.id));
|
|
|
12
12
|
export interface NewsFeedSettings {
|
|
13
13
|
userFeeds: RssFeedConfig[];
|
|
14
14
|
disabledDefaultFeedIds: string[];
|
|
15
|
-
|
|
15
|
+
needsMigration: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface UserNewsFeedInput {
|
|
@@ -109,6 +109,26 @@ function normalizeDisabledDefaultFeedIds(values: unknown[]): string[] {
|
|
|
109
109
|
return [...ids];
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function sameUserFeeds(raw: unknown, normalized: RssFeedConfig[]): boolean {
|
|
113
|
+
if (!Array.isArray(raw) || raw.length !== normalized.length) return false;
|
|
114
|
+
return raw.every((entry, index) => {
|
|
115
|
+
const feed = normalized[index];
|
|
116
|
+
if (!feed || !entry || typeof entry !== "object") return false;
|
|
117
|
+
const record = entry as Record<string, unknown>;
|
|
118
|
+
return record.id === feed.id
|
|
119
|
+
&& record.url === feed.url
|
|
120
|
+
&& record.name === feed.name
|
|
121
|
+
&& record.category === feed.category
|
|
122
|
+
&& record.authority === feed.authority
|
|
123
|
+
&& record.enabled === feed.enabled;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sameIdList(raw: unknown, normalized: string[]): boolean {
|
|
128
|
+
if (!Array.isArray(raw) || raw.length !== normalized.length) return false;
|
|
129
|
+
return raw.every((value, index) => value === normalized[index]);
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSettings {
|
|
113
133
|
const rawUserFeeds = configState.get<unknown>(USER_FEEDS_KEY);
|
|
114
134
|
const userFeedValues = parseJsonArray(rawUserFeeds);
|
|
@@ -123,11 +143,11 @@ export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSe
|
|
|
123
143
|
...parseJsonArray(rawLegacyDisabled),
|
|
124
144
|
]);
|
|
125
145
|
|
|
126
|
-
const
|
|
127
|
-
||
|
|
128
|
-
||
|
|
146
|
+
const needsMigration = rawLegacyDisabled !== null
|
|
147
|
+
|| (rawUserFeeds !== null && !sameUserFeeds(rawUserFeeds, userFeeds))
|
|
148
|
+
|| (rawDisabledIds !== null && !sameIdList(rawDisabledIds, disabledDefaultFeedIds));
|
|
129
149
|
|
|
130
|
-
return { userFeeds, disabledDefaultFeedIds,
|
|
150
|
+
return { userFeeds, disabledDefaultFeedIds, needsMigration };
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
export async function saveNewsFeedSettings(
|
|
@@ -41,13 +41,13 @@ function createNewsPresetPane(config: NewsPresetPaneConfig) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const TopPane = createNewsPresetPane({
|
|
44
|
-
paneKey: "top",
|
|
44
|
+
paneKey: "top:curated",
|
|
45
45
|
title: "Top News",
|
|
46
46
|
query: NEWS_QUERY_PRESETS.top,
|
|
47
47
|
columns: ["time", "source", "title", "tickers", "categories", "importance"],
|
|
48
48
|
defaultSort: { columnId: "importance", direction: "desc" },
|
|
49
49
|
emptyStateTitle: "No top stories yet",
|
|
50
|
-
emptyStateHint: "
|
|
50
|
+
emptyStateHint: "Top stories appear when curated market sources publish them.",
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
const FeedPane = createNewsPresetPane({
|
|
@@ -95,7 +95,7 @@ export const newsWireModule: PluginModule = {
|
|
|
95
95
|
],
|
|
96
96
|
setup(ctx) {
|
|
97
97
|
const initialSettings = loadNewsFeedSettings(ctx.configState);
|
|
98
|
-
if (initialSettings.
|
|
98
|
+
if (initialSettings.needsMigration) {
|
|
99
99
|
void saveNewsFeedSettings(ctx.configState, initialSettings);
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -2,7 +2,7 @@ import { Box } from "../../../../ui";
|
|
|
2
2
|
import { useEffect, useMemo } from "react";
|
|
3
3
|
import type { PaneProps } from "../../../../types/plugin";
|
|
4
4
|
import type { MarketNewsItem } from "../../../../types/news-source";
|
|
5
|
-
import { useLoadNewsStory, useNewsArticles } from "../../../../news/hooks";
|
|
5
|
+
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../news/hooks";
|
|
6
6
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../../runtime";
|
|
7
7
|
import { Tabs } from "../../../../components";
|
|
8
8
|
import { NewsDetailView, useNewsArticleDetail } from "./news/detail-view";
|
|
@@ -34,6 +34,7 @@ function useIndustryArticles(sector: SectorNewsSelection): {
|
|
|
34
34
|
allArticles: MarketNewsItem[];
|
|
35
35
|
loading: boolean;
|
|
36
36
|
error: string | null;
|
|
37
|
+
newsState: ReturnType<typeof useNewsArticles>;
|
|
37
38
|
} {
|
|
38
39
|
const allState = useNewsArticles(NEWS_QUERY_PRESETS.sectorAll);
|
|
39
40
|
const allArticles = usePersistedNewsArticles("industry:sector:all:articles", allState.articles);
|
|
@@ -50,6 +51,7 @@ function useIndustryArticles(sector: SectorNewsSelection): {
|
|
|
50
51
|
loading: allState.phase === "loading"
|
|
51
52
|
|| (allState.phase === "refreshing" && allArticles.length === 0),
|
|
52
53
|
error: allState.error,
|
|
54
|
+
newsState: allState,
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -57,7 +59,8 @@ export function IndustryPane({ focused, width, height }: PaneProps) {
|
|
|
57
59
|
const [category, setCategory] = usePluginPaneState<SectorNewsSelection>("industry:category", "all");
|
|
58
60
|
const [selectedArticleId, setSelectedArticleId] = useDebouncedPluginPaneState<string | null>("industry:selectedArticleId", null);
|
|
59
61
|
const [sortPreference, setSortPreference] = usePluginPaneState<NewsSortPreference>("industry:sort", DEFAULT_SORT);
|
|
60
|
-
const { articles, allArticles, loading, error } = useIndustryArticles(category);
|
|
62
|
+
const { articles, allArticles, loading, error, newsState } = useIndustryArticles(category);
|
|
63
|
+
const { scrollRef, onBodyScrollActivity } = useNewsTableLoadMore(NEWS_QUERY_PRESETS.sectorAll, newsState);
|
|
61
64
|
const loadNewsStory = useLoadNewsStory();
|
|
62
65
|
const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(articles, loadNewsStory);
|
|
63
66
|
const { readArticleIds, markArticleRead } = useNewsReadState();
|
|
@@ -140,6 +143,8 @@ export function IndustryPane({ focused, width, height }: PaneProps) {
|
|
|
140
143
|
})}
|
|
141
144
|
emptyStateTitle="No news in this category"
|
|
142
145
|
emptyStateHint="Try another category or wait for the next feed refresh."
|
|
146
|
+
scrollRef={scrollRef}
|
|
147
|
+
onBodyScrollActivity={onBodyScrollActivity}
|
|
143
148
|
/>
|
|
144
149
|
);
|
|
145
150
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
2
|
import type { PaneFooterSegment } from "../../../../../components";
|
|
3
3
|
import { t, tf } from "../../../../../i18n";
|
|
4
4
|
import { useAppLanguage } from "../../../../../i18n/react";
|
|
5
|
+
import { useShortcut } from "../../../../../react/input";
|
|
6
|
+
import { useUiCapabilities } from "../../../../../ui";
|
|
7
|
+
import { isPlainKey } from "../../../../../utils/keyboard";
|
|
5
8
|
import { useCloudAccessFooter } from "../../../shared/cloud-upgrade";
|
|
6
9
|
import { CLOUD_NEWS_DELAY_HOURS } from "../../../shared/plan-access";
|
|
7
10
|
import { usePaneStatusLinkFooter } from "../../../shared/pane-footer";
|
|
11
|
+
import { usePublicShare } from "../../../shared/public-share";
|
|
8
12
|
|
|
9
13
|
interface NewsFooterArticle {
|
|
14
|
+
title?: string | null;
|
|
15
|
+
summary?: string | null;
|
|
10
16
|
source?: string | null;
|
|
11
17
|
url?: string | null;
|
|
18
|
+
items?: Array<{ title?: string | null; summary?: string | null }>;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
interface UseNewsArticleFooterOptions {
|
|
@@ -29,6 +36,29 @@ export function useNewsArticleFooter({
|
|
|
29
36
|
error,
|
|
30
37
|
}: UseNewsArticleFooterOptions) {
|
|
31
38
|
const language = useAppLanguage();
|
|
39
|
+
const { publicSharing } = useUiCapabilities();
|
|
40
|
+
const createPublicShare = usePublicShare();
|
|
41
|
+
const shareArticle = useCallback(() => {
|
|
42
|
+
if (!article?.title) return;
|
|
43
|
+
const text = [
|
|
44
|
+
article.summary,
|
|
45
|
+
...(article.items ?? []).map((item) => item.summary || item.title),
|
|
46
|
+
].filter((value): value is string => !!value?.trim()).join("\n\n").slice(0, 50_000);
|
|
47
|
+
void createPublicShare({
|
|
48
|
+
kind: "article",
|
|
49
|
+
data: {
|
|
50
|
+
title: article.title,
|
|
51
|
+
text,
|
|
52
|
+
...(article.url ? { sourceUrl: article.url } : {}),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}, [article, createPublicShare]);
|
|
56
|
+
useShortcut((event) => {
|
|
57
|
+
if (!focused || !publicSharing || !article?.title || !isPlainKey(event, "y")) return;
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
shareArticle();
|
|
61
|
+
});
|
|
32
62
|
const { access, segment } = useCloudAccessFooter({
|
|
33
63
|
delayLabel: tf("{count}h", { count: CLOUD_NEWS_DELAY_HOURS }),
|
|
34
64
|
focused,
|
|
@@ -50,6 +80,9 @@ export function useNewsArticleFooter({
|
|
|
50
80
|
url: article?.url,
|
|
51
81
|
source: article?.source,
|
|
52
82
|
info: footerInfo,
|
|
83
|
+
hints: publicSharing && article?.title
|
|
84
|
+
? [{ id: "share", key: "y", label: " share", onPress: shareArticle }]
|
|
85
|
+
: undefined,
|
|
53
86
|
showOpenHint: true,
|
|
54
87
|
loading,
|
|
55
88
|
error,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box } from "../../../../../ui";
|
|
2
2
|
import type { NewsQuery } from "../../../../../news/types";
|
|
3
|
-
import { useLoadNewsStory, useNewsArticles } from "../../../../../news/hooks";
|
|
3
|
+
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
|
|
4
4
|
import type { PaneProps } from "../../../../../types/plugin";
|
|
5
5
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../../../runtime";
|
|
6
6
|
import { NewsDetailView, useNewsArticleDetail } from "./detail-view";
|
|
@@ -36,6 +36,7 @@ export function NewsPresetPane({
|
|
|
36
36
|
}) {
|
|
37
37
|
const newsState = useNewsArticles(query);
|
|
38
38
|
const articles = usePersistedNewsArticles(`${paneKey}:articles`, newsState.articles);
|
|
39
|
+
const { scrollRef, onBodyScrollActivity } = useNewsTableLoadMore(query, newsState);
|
|
39
40
|
// The aggregator opens a query in "loading", so the first paint is a loading
|
|
40
41
|
// body rather than a definitive empty wire.
|
|
41
42
|
const loading = newsState.phase === "loading"
|
|
@@ -102,6 +103,8 @@ export function NewsPresetPane({
|
|
|
102
103
|
})}
|
|
103
104
|
emptyStateTitle={emptyStateTitle}
|
|
104
105
|
emptyStateHint={emptyStateHint}
|
|
106
|
+
scrollRef={scrollRef}
|
|
107
|
+
onBodyScrollActivity={onBodyScrollActivity}
|
|
105
108
|
/>
|
|
106
109
|
);
|
|
107
110
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, type ReactNode } from "react";
|
|
2
|
-
import { TextAttributes } from "../../../../../ui";
|
|
1
|
+
import { useCallback, useEffect, useMemo, type ReactNode, type RefObject } from "react";
|
|
2
|
+
import { TextAttributes, type ScrollBoxRenderable } from "../../../../../ui";
|
|
3
3
|
import {
|
|
4
4
|
DataTableStackView,
|
|
5
5
|
PaneStatusBody,
|
|
@@ -96,6 +96,8 @@ interface NewsArticleStackBaseProps {
|
|
|
96
96
|
emptyStateTitle: string;
|
|
97
97
|
emptyStateHint?: string;
|
|
98
98
|
titleForArticle?: (article: MarketNewsItem) => string;
|
|
99
|
+
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
100
|
+
onBodyScrollActivity?: () => void;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
function compareText(a: string, b: string): number {
|
|
@@ -223,6 +225,8 @@ export function NewsArticleStackView({
|
|
|
223
225
|
onArticleRead,
|
|
224
226
|
detailOpen,
|
|
225
227
|
onBack,
|
|
228
|
+
scrollRef,
|
|
229
|
+
onBodyScrollActivity,
|
|
226
230
|
detailContent,
|
|
227
231
|
detailTitle,
|
|
228
232
|
rootBefore,
|
|
@@ -349,6 +353,8 @@ export function NewsArticleStackView({
|
|
|
349
353
|
emptyStateTitle={emptyStateTitle}
|
|
350
354
|
emptyStateHint={emptyStateHint}
|
|
351
355
|
showHorizontalScrollbar={false}
|
|
356
|
+
scrollRef={scrollRef}
|
|
357
|
+
onBodyScrollActivity={onBodyScrollActivity}
|
|
352
358
|
/>
|
|
353
359
|
);
|
|
354
360
|
}
|
|
@@ -20,6 +20,18 @@ const RSS_FIXTURE = `<rss version="2.0"><channel><item>
|
|
|
20
20
|
</item></channel></rss>`;
|
|
21
21
|
|
|
22
22
|
describe("createRssNewsCapability", () => {
|
|
23
|
+
test("does not serve unclassified RSS stories as top news", async () => {
|
|
24
|
+
const fetchText = mock(async () => ({
|
|
25
|
+
ok: true,
|
|
26
|
+
text: async () => RSS_FIXTURE,
|
|
27
|
+
}));
|
|
28
|
+
const source = createRssNewsCapability([FEED], { fetchText });
|
|
29
|
+
|
|
30
|
+
expect(source.provider.supports?.({ feed: "top" })).toBe(false);
|
|
31
|
+
expect(await source.provider.fetchNews({ feed: "top" })).toEqual([]);
|
|
32
|
+
expect(fetchText).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
23
35
|
test("caches fetched feed items with feed authority scoring", async () => {
|
|
24
36
|
const persistence = new MemoryPersistence();
|
|
25
37
|
const fetchText = mock(async () => ({
|
|
@@ -37,7 +37,7 @@ export interface RssNewsCapabilityOptions {
|
|
|
37
37
|
|
|
38
38
|
function supportsQuery(query: NewsQuery): boolean {
|
|
39
39
|
const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
|
|
40
|
-
return feed === "latest"
|
|
40
|
+
return feed === "latest";
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function serializeItem(item: MarketNewsItem): CachedNewsItem {
|
|
@@ -4,7 +4,15 @@ export interface QuickNoteEntry {
|
|
|
4
4
|
updatedAt?: number;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/** A stored note: `key` is what `load`/`save` take, not the file path. */
|
|
8
|
+
export interface NoteFileEntry {
|
|
9
|
+
key: string;
|
|
10
|
+
text: string;
|
|
11
|
+
updatedAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const QUICK_NOTES_INDEX = "__quick-notes-index__";
|
|
15
|
+
const LOCAL_TIMESTAMP_KEY = "gloomberb:notes:__updated-at__";
|
|
8
16
|
|
|
9
17
|
function joinPath(...parts: string[]): string {
|
|
10
18
|
return parts.join("/").replace(/\/+/g, "/");
|
|
@@ -29,6 +37,28 @@ async function readTextFile(path: string): Promise<string> {
|
|
|
29
37
|
return getLocalStorage()?.getItem(`gloomberb:notes:${path}`) ?? "";
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
/** Browsers have no mtime, so note writes keep their own timestamp index. */
|
|
41
|
+
function readLocalTimestamps(): Record<string, number> {
|
|
42
|
+
try {
|
|
43
|
+
const raw = getLocalStorage()?.getItem(LOCAL_TIMESTAMP_KEY);
|
|
44
|
+
const parsed: unknown = raw ? JSON.parse(raw) : null;
|
|
45
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
46
|
+
? parsed as Record<string, number>
|
|
47
|
+
: {};
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeLocalTimestamp(path: string, updatedAt: number | null): void {
|
|
54
|
+
const storage = getLocalStorage();
|
|
55
|
+
if (!storage) return;
|
|
56
|
+
const timestamps = readLocalTimestamps();
|
|
57
|
+
if (updatedAt == null) delete timestamps[path];
|
|
58
|
+
else timestamps[path] = updatedAt;
|
|
59
|
+
try { storage.setItem(LOCAL_TIMESTAMP_KEY, JSON.stringify(timestamps)); } catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
async function writeTextFile(path: string, value: string): Promise<void> {
|
|
33
63
|
if (typeof Bun !== "undefined") {
|
|
34
64
|
const fsModulePath = "fs/promises";
|
|
@@ -37,6 +67,7 @@ async function writeTextFile(path: string, value: string): Promise<void> {
|
|
|
37
67
|
return;
|
|
38
68
|
}
|
|
39
69
|
getLocalStorage()?.setItem(`gloomberb:notes:${path}`, value);
|
|
70
|
+
writeLocalTimestamp(path, Date.now());
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
async function deleteTextFile(path: string): Promise<void> {
|
|
@@ -47,6 +78,7 @@ async function deleteTextFile(path: string): Promise<void> {
|
|
|
47
78
|
return;
|
|
48
79
|
}
|
|
49
80
|
getLocalStorage()?.removeItem(`gloomberb:notes:${path}`);
|
|
81
|
+
writeLocalTimestamp(path, null);
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
export class NotesFiles {
|
|
@@ -82,6 +114,54 @@ export class NotesFiles {
|
|
|
82
114
|
}
|
|
83
115
|
}
|
|
84
116
|
|
|
117
|
+
/** Every stored note, ticker notes and quick notes alike, newest first. */
|
|
118
|
+
async list(): Promise<NoteFileEntry[]> {
|
|
119
|
+
const entries: NoteFileEntry[] = [];
|
|
120
|
+
for (const [key, updatedAt] of await this.listKeys()) {
|
|
121
|
+
try {
|
|
122
|
+
entries.push({ key, text: await this.load(key), updatedAt });
|
|
123
|
+
} catch {
|
|
124
|
+
// An unreadable note is skipped rather than synced as empty.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return entries.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async listKeys(): Promise<Array<[string, number]>> {
|
|
131
|
+
if (typeof Bun !== "undefined") {
|
|
132
|
+
const fsModulePath = "fs/promises";
|
|
133
|
+
const { readdir, stat } = await import(fsModulePath) as typeof import("fs/promises");
|
|
134
|
+
let names: string[];
|
|
135
|
+
try {
|
|
136
|
+
names = await readdir(this.dataDir);
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const keys: Array<[string, number]> = [];
|
|
141
|
+
for (const name of names) {
|
|
142
|
+
if (!name.endsWith(".md")) continue;
|
|
143
|
+
try {
|
|
144
|
+
const info = await stat(joinPath(this.dataDir, name));
|
|
145
|
+
keys.push([name.slice(0, -3), Math.round(info.mtimeMs)]);
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
return keys;
|
|
149
|
+
}
|
|
150
|
+
const prefix = joinPath(this.dataDir, "");
|
|
151
|
+
const timestamps = readLocalTimestamps();
|
|
152
|
+
const storageKeyPrefix = `gloomberb:notes:${prefix}`;
|
|
153
|
+
// Notes written before the index existed are stamped now rather than at
|
|
154
|
+
// the epoch, so an older cloud copy cannot overwrite them on first sync.
|
|
155
|
+
for (const storageKey of Object.keys(globalThis.localStorage ?? {})) {
|
|
156
|
+
if (!storageKey.startsWith(storageKeyPrefix) || !storageKey.endsWith(".md")) continue;
|
|
157
|
+
const path = storageKey.slice("gloomberb:notes:".length);
|
|
158
|
+
if (timestamps[path] == null) writeLocalTimestamp(path, timestamps[path] = Date.now());
|
|
159
|
+
}
|
|
160
|
+
return Object.entries(timestamps)
|
|
161
|
+
.filter(([path]) => path.startsWith(prefix) && path.endsWith(".md"))
|
|
162
|
+
.map(([path, updatedAt]) => [path.slice(prefix.length, -3), updatedAt]);
|
|
163
|
+
}
|
|
164
|
+
|
|
85
165
|
private indexPath(): string {
|
|
86
166
|
return joinPath(this.dataDir, `${QUICK_NOTES_INDEX}.json`);
|
|
87
167
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
2
2
|
import { NotesFiles } from "./files";
|
|
3
3
|
import { createQuickNotesPane } from "./quick-notes-pane";
|
|
4
|
+
import { createNotesSyncContributor } from "./sync";
|
|
4
5
|
import { createNotesTab } from "./ticker-notes-tab";
|
|
5
6
|
|
|
6
7
|
export const notesPlugin: GloomPlugin = {
|
|
@@ -22,6 +23,8 @@ export const notesPlugin: GloomPlugin = {
|
|
|
22
23
|
});
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
ctx.registerSyncContributor(createNotesSyncContributor(notesFiles));
|
|
27
|
+
|
|
25
28
|
ctx.registerTickerResearchTab({
|
|
26
29
|
id: "notes",
|
|
27
30
|
name: "Notes",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import type { SyncApplyContext } from "../../../sync/types";
|
|
6
|
+
import { NotesFiles } from "./files";
|
|
7
|
+
import { createNotesSyncContributor } from "./sync";
|
|
8
|
+
|
|
9
|
+
const directories: string[] = [];
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
for (const directory of directories.splice(0)) {
|
|
13
|
+
await rm(directory, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function createNotesFiles(): Promise<NotesFiles> {
|
|
18
|
+
const directory = await mkdtemp(join(tmpdir(), "gloomberb-notes-sync-"));
|
|
19
|
+
directories.push(directory);
|
|
20
|
+
return new NotesFiles(directory);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const applyContext = { isCurrent: () => true } as unknown as SyncApplyContext;
|
|
24
|
+
|
|
25
|
+
test("resolves each note to whichever side wrote it last", async () => {
|
|
26
|
+
const notesFiles = await createNotesFiles();
|
|
27
|
+
await notesFiles.save("AAPL", "local aapl");
|
|
28
|
+
await notesFiles.save("MSFT", "local msft");
|
|
29
|
+
const contributor = createNotesSyncContributor(notesFiles);
|
|
30
|
+
|
|
31
|
+
await contributor.apply?.({
|
|
32
|
+
notes: [
|
|
33
|
+
{ key: "AAPL", text: "remote aapl", updatedAt: Date.now() + 60_000 },
|
|
34
|
+
{ key: "MSFT", text: "remote msft", updatedAt: Date.now() - 60_000 },
|
|
35
|
+
{ key: "TSLA", text: "remote tsla", updatedAt: Date.now() },
|
|
36
|
+
],
|
|
37
|
+
quickNotes: [{ id: "note-1", title: "Remote", updatedAt: Date.now() }],
|
|
38
|
+
}, applyContext);
|
|
39
|
+
|
|
40
|
+
expect(await notesFiles.load("AAPL")).toBe("remote aapl");
|
|
41
|
+
expect(await notesFiles.load("MSFT")).toBe("local msft");
|
|
42
|
+
expect(await notesFiles.load("TSLA")).toBe("remote tsla");
|
|
43
|
+
expect(await notesFiles.loadQuickNotesIndex()).toEqual([
|
|
44
|
+
{ id: "note-1", title: "Remote", updatedAt: expect.any(Number) },
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const payload = await contributor.collect({} as never) as { notes: Array<{ key: string }> };
|
|
48
|
+
expect(payload.notes.map((note) => note.key).sort()).toEqual(["AAPL", "MSFT", "TSLA"]);
|
|
49
|
+
});
|