gloomberb 0.17.4 → 0.17.5
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 +1 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/data.ts +4 -1
- package/src/api-client/research-activity.ts +10 -3
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/update.ts +50 -3
- package/src/app.tsx +2 -0
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/glossary.ts +34 -4
- package/src/cli/index.ts +6 -0
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/components/chart/composite/format.ts +7 -0
- package/src/components/chart/composite/legend.tsx +8 -11
- package/src/components/command-bar/commands/direct/index.ts +47 -0
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +16 -1
- package/src/components/command-bar/surface/index.tsx +2 -1
- package/src/components/command-bar/workflow/ops.ts +4 -3
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/fields.tsx +5 -0
- package/src/data/config/store/normalize.ts +8 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/zh-cn.ts +2 -0
- package/src/i18n/zh-tw.ts +2 -0
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +4 -0
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/hooks.tsx +168 -29
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +20 -2
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +20 -0
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/state.ts +15 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +2 -0
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/futures-curve/headless.ts +13 -6
- package/src/plugins/builtin/futures-curve/index.tsx +3 -3
- package/src/plugins/builtin/futures-curve/model.ts +64 -4
- package/src/plugins/builtin/futures-curve/pane.tsx +11 -4
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/options-positioning/model.ts +14 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +17 -10
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -12
- package/src/renderers/electrobun/shared/protocol.ts +3 -2
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/time-series/resolve.ts +21 -6
- package/src/time-series/spec.ts +8 -0
- package/src/types/config.ts +14 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +125 -16
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/semver.ts +47 -2
|
@@ -135,6 +135,125 @@ function buildTickerFinancialsKeys(tickers: TickerRecord[], options: TickerInstr
|
|
|
135
135
|
return keys;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* A new object so memo dependencies move, over the map that already holds
|
|
140
|
+
* every unchanged entry. Copying that map would visit every instrument.
|
|
141
|
+
*/
|
|
142
|
+
class RetainedMapView<K, V> extends Map<K, V> {
|
|
143
|
+
constructor(private readonly backing: Map<K, V>) {
|
|
144
|
+
super();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override get size(): number {
|
|
148
|
+
return this.backing.size;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override get(key: K): V | undefined {
|
|
152
|
+
return this.backing.get(key);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override has(key: K): boolean {
|
|
156
|
+
return this.backing.has(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override forEach(callback: (value: V, key: K, map: Map<K, V>) => void, thisArg?: unknown): void {
|
|
160
|
+
this.backing.forEach((value, key) => {
|
|
161
|
+
callback.call(thisArg, value, key, this);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override keys(): MapIterator<K> {
|
|
166
|
+
return this.backing.keys();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override values(): MapIterator<V> {
|
|
170
|
+
return this.backing.values();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
override entries(): MapIterator<[K, V]> {
|
|
174
|
+
return this.backing.entries();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
override [Symbol.iterator](): MapIterator<[K, V]> {
|
|
178
|
+
return this.backing[Symbol.iterator]();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface PatchedMapSlot<K, V> {
|
|
183
|
+
key: K;
|
|
184
|
+
versionKeys: readonly string[];
|
|
185
|
+
read: () => V | null | undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface PatchedMapState<K, V> {
|
|
189
|
+
signature: string;
|
|
190
|
+
coordinator: SharedCoordinator | null;
|
|
191
|
+
slots: Array<PatchedMapSlot<K, V>>;
|
|
192
|
+
versions: number[];
|
|
193
|
+
backing: Map<K, V>;
|
|
194
|
+
view: Map<K, V>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function slotVersion(
|
|
198
|
+
slot: PatchedMapSlot<unknown, unknown>,
|
|
199
|
+
versionOf: ((key: string) => number) | null,
|
|
200
|
+
aggregate: number,
|
|
201
|
+
): number {
|
|
202
|
+
if (!versionOf) return aggregate;
|
|
203
|
+
let version = 0;
|
|
204
|
+
for (const key of slot.versionKeys) version += versionOf(key);
|
|
205
|
+
return version;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function usePatchedMap<K, V>(
|
|
209
|
+
signature: string,
|
|
210
|
+
coordinator: SharedCoordinator | null,
|
|
211
|
+
aggregateVersion: number,
|
|
212
|
+
versionOf: ((key: string) => number) | null,
|
|
213
|
+
createSlots: () => Array<PatchedMapSlot<K, V>>,
|
|
214
|
+
): Map<K, V> {
|
|
215
|
+
const stateRef = useRef<PatchedMapState<K, V> | null>(null);
|
|
216
|
+
let state = stateRef.current;
|
|
217
|
+
if (!state || state.signature !== signature || state.coordinator !== coordinator) {
|
|
218
|
+
const slots = createSlots();
|
|
219
|
+
const backing = new Map<K, V>();
|
|
220
|
+
const versions: number[] = [];
|
|
221
|
+
for (const slot of slots) {
|
|
222
|
+
const value = slot.read();
|
|
223
|
+
if (value != null) backing.set(slot.key, value);
|
|
224
|
+
versions.push(slotVersion(slot, versionOf, aggregateVersion));
|
|
225
|
+
}
|
|
226
|
+
const view = new RetainedMapView(backing);
|
|
227
|
+
state = { signature, coordinator, slots, versions, backing, view };
|
|
228
|
+
stateRef.current = state;
|
|
229
|
+
return view;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let changed = false;
|
|
233
|
+
for (let index = 0; index < state.slots.length; index += 1) {
|
|
234
|
+
const slot = state.slots[index]!;
|
|
235
|
+
const version = slotVersion(slot, versionOf, aggregateVersion);
|
|
236
|
+
if (version === state.versions[index]) continue;
|
|
237
|
+
state.versions[index] = version;
|
|
238
|
+
const value = slot.read();
|
|
239
|
+
if (value == null) {
|
|
240
|
+
if (state.backing.delete(slot.key)) changed = true;
|
|
241
|
+
} else if (state.backing.get(slot.key) !== value) {
|
|
242
|
+
state.backing.set(slot.key, value);
|
|
243
|
+
changed = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!changed) return state.view;
|
|
247
|
+
const view = new RetainedMapView(state.backing);
|
|
248
|
+
state.view = view;
|
|
249
|
+
return view;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function versionOfCoordinator(coordinator: SharedCoordinator | null): ((key: string) => number) | null {
|
|
253
|
+
if (!coordinator || typeof coordinator.getKeyVersion !== "function") return null;
|
|
254
|
+
return (key) => coordinator.getKeyVersion(key);
|
|
255
|
+
}
|
|
256
|
+
|
|
138
257
|
export function useTickerFinancialsMap(
|
|
139
258
|
tickers: TickerRecord[],
|
|
140
259
|
options: TickerInstrumentOptions = {},
|
|
@@ -145,19 +264,30 @@ export function useTickerFinancialsMap(
|
|
|
145
264
|
const subscriptionKeys = useMemo(() => buildTickerFinancialsKeys(tickers, options), [optionsKey, tickerKey]);
|
|
146
265
|
const keysVersion = useCoordinatorKeysVersion(subscriptionKeys);
|
|
147
266
|
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
267
|
+
return usePatchedMap(
|
|
268
|
+
`${tickerKey}\0${optionsKey}`,
|
|
269
|
+
coordinator,
|
|
270
|
+
keysVersion,
|
|
271
|
+
versionOfCoordinator(coordinator),
|
|
272
|
+
() => {
|
|
273
|
+
if (!coordinator) return [];
|
|
274
|
+
const slots: Array<PatchedMapSlot<string, TickerFinancials>> = [];
|
|
275
|
+
for (const ticker of tickers) {
|
|
276
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
|
|
277
|
+
if (!instrument) continue;
|
|
278
|
+
slots.push({
|
|
279
|
+
key: ticker.metadata.ticker,
|
|
280
|
+
versionKeys: [
|
|
281
|
+
buildSnapshotKey(instrument),
|
|
282
|
+
buildQuoteKey(instrument),
|
|
283
|
+
buildChartKey(createBaselineChartRequest(instrument)),
|
|
284
|
+
],
|
|
285
|
+
read: () => coordinator.getTickerFinancialsSync(instrument),
|
|
286
|
+
});
|
|
157
287
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
288
|
+
return slots;
|
|
289
|
+
},
|
|
290
|
+
);
|
|
161
291
|
}
|
|
162
292
|
|
|
163
293
|
export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
|
|
@@ -197,15 +327,23 @@ export function useQuoteEntries(
|
|
|
197
327
|
const keysVersion = useCoordinatorKeysVersion(keys);
|
|
198
328
|
const coordinator = getSharedMarketDataCoordinator();
|
|
199
329
|
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
]
|
|
207
|
-
|
|
208
|
-
|
|
330
|
+
return usePatchedMap(
|
|
331
|
+
instrumentKey,
|
|
332
|
+
coordinator,
|
|
333
|
+
keysVersion,
|
|
334
|
+
versionOfCoordinator(coordinator),
|
|
335
|
+
() => {
|
|
336
|
+
if (!coordinator) return [];
|
|
337
|
+
return instruments.map((instrument) => {
|
|
338
|
+
const key = buildQuoteKey(instrument);
|
|
339
|
+
return {
|
|
340
|
+
key,
|
|
341
|
+
versionKeys: [key],
|
|
342
|
+
read: () => coordinator.getQuoteEntry(instrument),
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
);
|
|
209
347
|
}
|
|
210
348
|
|
|
211
349
|
export function useChartQueries(
|
|
@@ -219,7 +357,8 @@ export function useChartQueries(
|
|
|
219
357
|
const keysVersion = useCoordinatorKeysVersion(keys);
|
|
220
358
|
const coordinator = getSharedMarketDataCoordinator();
|
|
221
359
|
const appActive = useAppVisible();
|
|
222
|
-
const
|
|
360
|
+
const paneVisible = usePaneVisible();
|
|
361
|
+
const wasPaneVisibleRef = useRef(paneVisible);
|
|
223
362
|
const entries = useMemo(() => (
|
|
224
363
|
coordinator
|
|
225
364
|
? requests.map((request) => [buildChartKey(request), coordinator.getChartEntry(request)] as const)
|
|
@@ -229,15 +368,15 @@ export function useChartQueries(
|
|
|
229
368
|
useEffect(() => {
|
|
230
369
|
const coordinator = getSharedMarketDataCoordinator();
|
|
231
370
|
if (!coordinator) {
|
|
232
|
-
|
|
371
|
+
wasPaneVisibleRef.current = paneVisible;
|
|
233
372
|
return;
|
|
234
373
|
}
|
|
235
374
|
if (!appActive) {
|
|
236
|
-
|
|
375
|
+
wasPaneVisibleRef.current = false;
|
|
237
376
|
return;
|
|
238
377
|
}
|
|
239
|
-
const forceRefresh = refreshIntervalMs > 0 && !
|
|
240
|
-
|
|
378
|
+
const forceRefresh = refreshIntervalMs > 0 && paneVisible && !wasPaneVisibleRef.current;
|
|
379
|
+
wasPaneVisibleRef.current = paneVisible;
|
|
241
380
|
const loadRequests = () => {
|
|
242
381
|
loadChartRequests(coordinator, requests, { forceRefresh });
|
|
243
382
|
};
|
|
@@ -247,17 +386,17 @@ export function useChartQueries(
|
|
|
247
386
|
}
|
|
248
387
|
const timeout = setTimeout(loadRequests, debounceMs);
|
|
249
388
|
return () => clearTimeout(timeout);
|
|
250
|
-
}, [appActive, debounceMs, refreshIntervalMs, requestKey]);
|
|
389
|
+
}, [appActive, debounceMs, paneVisible, refreshIntervalMs, requestKey]);
|
|
251
390
|
|
|
252
391
|
useEffect(() => {
|
|
253
392
|
const coordinator = getSharedMarketDataCoordinator();
|
|
254
|
-
if (!coordinator || !
|
|
393
|
+
if (!coordinator || !paneVisible || refreshIntervalMs <= 0 || requests.length === 0) return;
|
|
255
394
|
|
|
256
395
|
const interval = setInterval(() => {
|
|
257
396
|
loadChartRequests(coordinator, requests, { forceRefresh: true });
|
|
258
397
|
}, refreshIntervalMs);
|
|
259
398
|
return () => clearInterval(interval);
|
|
260
|
-
}, [
|
|
399
|
+
}, [paneVisible, refreshIntervalMs, requestKey]);
|
|
261
400
|
|
|
262
401
|
return useMemo(() => new Map(entries), [entries]);
|
|
263
402
|
}
|
|
@@ -91,8 +91,8 @@ function BrokerConfigFieldEditor({
|
|
|
91
91
|
function accountDetail(account: BrokerAccount): string {
|
|
92
92
|
const parts = [
|
|
93
93
|
account.accountId,
|
|
94
|
-
account.netLiquidation != null ? tf("{value} net liq", { value: formatCurrency(account.netLiquidation, account.currency || "
|
|
95
|
-
account.buyingPower != null ? tf("{value} buying power", { value: formatCurrency(account.buyingPower, account.currency || "
|
|
94
|
+
account.netLiquidation != null ? tf("{value} net liq", { value: formatCurrency(account.netLiquidation, account.currency || "") }) : null,
|
|
95
|
+
account.buyingPower != null ? tf("{value} buying power", { value: formatCurrency(account.buyingPower, account.currency || "") }) : null,
|
|
96
96
|
];
|
|
97
97
|
return parts.filter(Boolean).join(" · ");
|
|
98
98
|
}
|
|
@@ -46,10 +46,10 @@ function formatBrokerMode(value: unknown): string {
|
|
|
46
46
|
|
|
47
47
|
function summarizeBrokerAccounts(accounts: BrokerAccount[]): string {
|
|
48
48
|
if (accounts.length === 0) return "0 acct";
|
|
49
|
-
const currencies = new Set(accounts.map((account) => account.currency || "
|
|
49
|
+
const currencies = new Set(accounts.map((account) => account.currency || ""));
|
|
50
50
|
const canSum = currencies.size === 1 && accounts.every((account) => typeof account.netLiquidation === "number");
|
|
51
51
|
if (!canSum) return `${accounts.length} acct${accounts.length === 1 ? "" : "s"}`;
|
|
52
|
-
const currency = currencies.values().next().value ?? "
|
|
52
|
+
const currency = currencies.values().next().value ?? "";
|
|
53
53
|
const total = accounts.reduce((sum, account) => sum + (account.netLiquidation ?? 0), 0);
|
|
54
54
|
return formatCurrency(total, currency);
|
|
55
55
|
}
|
|
@@ -27,6 +27,7 @@ import { parseChartSpec } from "./chart-spec";
|
|
|
27
27
|
import { paneSchemas } from "./headless-schema";
|
|
28
28
|
import { barHistoryFreshness, REPORTED_DATA } from "../shared/report-freshness";
|
|
29
29
|
import { inferSeriesFrequency } from "../../../series/csv";
|
|
30
|
+
import { isDatasetChart } from "../../../time-series/spec";
|
|
30
31
|
|
|
31
32
|
export interface ChartPaneModel extends HeadlessSeriesResult {
|
|
32
33
|
/** The renderer consumes the full model; generic reports project series and metadata. */
|
|
@@ -285,7 +286,7 @@ function seriesAttribution(series: readonly HeadlessSeries[]): Pick<HeadlessPane
|
|
|
285
286
|
function chartFreshness(series: readonly HeadlessSeries[], spec: ChartSpec): HeadlessPaneFreshness {
|
|
286
287
|
const order = ["1wk", "1mo"];
|
|
287
288
|
const resolutions = series.map((entry) => (entry as HeadlessSeries & { historyResolution?: string }).historyResolution);
|
|
288
|
-
const dataset = spec
|
|
289
|
+
const dataset = isDatasetChart(spec);
|
|
289
290
|
if (!series.length || resolutions.some((resolution) => !resolution)) {
|
|
290
291
|
return { ...REPORTED_DATA, basis: "reported and published data", ...(dataset ? { dataset } : {}), ...economicChartDates(series, spec), ...seriesAttribution(series) };
|
|
291
292
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type MultiSelectRowAction,
|
|
19
19
|
} from "../../../components/ui";
|
|
20
20
|
import { CompositeChart } from "../../../components/chart/composite";
|
|
21
|
+
import { formatCompositeDatasetDate } from "../../../components/chart/composite/format";
|
|
21
22
|
import type { CompositeChartLevels, CompositeLevelEdit } from "../../../components/chart/composite/levels";
|
|
22
23
|
import type { PaneProps, TickerResearchTabProps } from "../../../types/plugin";
|
|
23
24
|
import type { ChartResolution, TimeRange } from "../../../components/chart/core/types";
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
import { colors } from "../../../theme/colors";
|
|
47
48
|
import { isUsListingExchange, parsePublicTickerKey, publicTickerKey } from "../../../utils/exchanges";
|
|
48
49
|
import { isMarketFieldId } from "../../../time-series/field-catalog";
|
|
50
|
+
import { isDatasetChart } from "../../../time-series/spec";
|
|
49
51
|
import { CHART_COMPOSER_PANE_ID } from "../../../types/config";
|
|
50
52
|
import { useRemoteUiNode } from "../../../remote/semantic-tree";
|
|
51
53
|
import { SeriesEditorDialog } from "./editor";
|
|
@@ -934,12 +936,28 @@ function ChartComposerSurface({
|
|
|
934
936
|
}, [anchoredVwapOn]);
|
|
935
937
|
const anchorable = anchoredVwapOn && !pickingAnchor;
|
|
936
938
|
const footerAnchor = useCallback(() => setPickingAnchor(true), []);
|
|
939
|
+
// A dataset (CHOKE's transits) panned or zoomed back from its newest
|
|
940
|
+
// observation says how far its data goes, as its legend dates a value.
|
|
941
|
+
const newestBeyondView = useMemo(() => {
|
|
942
|
+
const end = viewport?.end.getTime();
|
|
943
|
+
if (!activeRuntimeViewport || !isDatasetChart(spec) || end === undefined || !Number.isFinite(end)) return null;
|
|
944
|
+
let newest = Number.NEGATIVE_INFINITY;
|
|
945
|
+
for (const entry of plottedSeries) {
|
|
946
|
+
for (const point of entry.points) {
|
|
947
|
+
const time = point.date.getTime();
|
|
948
|
+
if (point.value != null && Number.isFinite(time) && time > newest) newest = time;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return newest > end ? formatCompositeDatasetDate(new Date(newest), end) : null;
|
|
952
|
+
}, [activeRuntimeViewport, plottedSeries, spec, viewport]);
|
|
937
953
|
usePaneFooter(footerId, () => ({
|
|
938
954
|
info: resolution.loading
|
|
939
955
|
? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }]
|
|
940
956
|
: pickingAnchor
|
|
941
957
|
? [{ id: "anchor", parts: [{ text: "pick a bar to anchor VWAP", tone: "muted" as const }] }]
|
|
942
|
-
:
|
|
958
|
+
: newestBeyondView
|
|
959
|
+
? [{ id: "newest", asOf: true, parts: [{ text: `newest ${newestBeyondView}`, tone: "muted" as const }] }]
|
|
960
|
+
: [],
|
|
943
961
|
hints: [
|
|
944
962
|
{ id: "series", key: "s", label: "eries", onPress: footerSeries },
|
|
945
963
|
{ id: "indicators", key: "i", label: "ndicators", onPress: openIndicators, disabled: indicatorsDisabled },
|
|
@@ -947,7 +965,7 @@ function ChartComposerSurface({
|
|
|
947
965
|
{ id: "resolution", key: "t", label: "imeframe", onPress: footerResolution },
|
|
948
966
|
...(anchorable ? [{ id: "vwap-anchor", key: "v", label: "wap anchor", title: "Anchor VWAP", onPress: footerAnchor }] : []),
|
|
949
967
|
],
|
|
950
|
-
}), [resolution.loading, pickingAnchor, footerSeries, openIndicators, indicatorsDisabled, openFormulas, formulasDisabled, footerResolution, anchorable, footerAnchor]);
|
|
968
|
+
}), [resolution.loading, pickingAnchor, newestBeyondView, footerSeries, openIndicators, indicatorsDisabled, openFormulas, formulasDisabled, footerResolution, anchorable, footerAnchor]);
|
|
951
969
|
|
|
952
970
|
// A fixed window (a GIP session) highlights no range, so the bar names it.
|
|
953
971
|
const dateWindowLabel = useMemo(() => {
|
|
@@ -16,6 +16,8 @@ import { canReactToMessage } from "../reactions";
|
|
|
16
16
|
* The chat pane's keys, shown in its footer and listed in its pane menu. The
|
|
17
17
|
* hints are the bindings for everything but Enter and `i`, which the chat's
|
|
18
18
|
* own handler owns because they depend on the composer and the selection.
|
|
19
|
+
* With no message selected both open the composer, as a click on it does, so
|
|
20
|
+
* the footer leaves that out and only the pane menu lists Compose.
|
|
19
21
|
*/
|
|
20
22
|
export function useChatFooter({
|
|
21
23
|
composing,
|
|
@@ -240,8 +242,6 @@ export function useChatFooter({
|
|
|
240
242
|
if (openTargets.length > 0) hints.push({ id: "open", key: "o", label: "pen", title: openTargetTitle, onPress: () => { void latest.current.openSelectedTargets(); } });
|
|
241
243
|
// Every author but a Discord ghost has a card, if only their name and a way to write to them.
|
|
242
244
|
if (author) hints.push({ id: "profile", key: "p", label: "rofile", title: "Show Profile", onPress: () => latest.current.toggleAuthorProfile() });
|
|
243
|
-
} else if (!composing && canSend) {
|
|
244
|
-
hints.push({ id: "compose", key: "i", label: " compose", title: "Compose", onPress: () => queueMicrotask(() => latest.current.focusComposer()) });
|
|
245
245
|
}
|
|
246
246
|
if (canSend && notificationChannelId) {
|
|
247
247
|
const channelId = notificationChannelId;
|
|
@@ -261,6 +261,9 @@ export function useChatFooter({
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
const menu: ContextMenuItem[] = [];
|
|
264
|
+
if (!composing && !selectedMessage && canSend) {
|
|
265
|
+
menu.push({ id: "compose", label: "Compose", accelerator: "Enter", onSelect: () => queueMicrotask(() => latest.current.focusComposer()) });
|
|
266
|
+
}
|
|
264
267
|
// No key of its own: a stray press would start a conversation. The pane
|
|
265
268
|
// menu (.) and a right-click on the name reach it.
|
|
266
269
|
if (authorMessageLabel) {
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
} from "../edit-window";
|
|
69
69
|
import { ChatControllerRealtime } from "./realtime";
|
|
70
70
|
import { ChatControllerChannels } from "./channels";
|
|
71
|
+
import { ChatControllerUnreadPeople } from "./unread-people";
|
|
71
72
|
import { ChatControllerView } from "./view";
|
|
72
73
|
import { ChatControllerMessageLoading } from "./message-loading";
|
|
73
74
|
import { ChatControllerStorage } from "./storage";
|
|
@@ -149,6 +150,13 @@ export class ChatController {
|
|
|
149
150
|
getListenerSnapshot: (channelId) => this.getSnapshot(channelId),
|
|
150
151
|
getVisibleMessages: (channelId) => this.getVisibleMessages(channelId),
|
|
151
152
|
getUnreadMentionCount: (channelId) => this.getUnreadMentionCount(channelId),
|
|
153
|
+
getUnreadPeople: () => this.unreadPeople.snapshot(),
|
|
154
|
+
});
|
|
155
|
+
private readonly unreadPeople = new ChatControllerUnreadPeople({
|
|
156
|
+
getChannels: () => this.channelCatalog.getChannels(),
|
|
157
|
+
getChannelStates: () => this.storage.channelStates.entries(),
|
|
158
|
+
getUser: () => this.session.user,
|
|
159
|
+
onChange: () => this.emit(),
|
|
152
160
|
});
|
|
153
161
|
private readonly messageLoading = new ChatControllerMessageLoading({
|
|
154
162
|
ensureChannelState: (channelId) => this.ensureChannelState(channelId),
|
|
@@ -247,6 +255,16 @@ export class ChatController {
|
|
|
247
255
|
return this.view.getSnapshot(channelId);
|
|
248
256
|
}
|
|
249
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Keeps who sent each channel's unread messages (`unreadPeople` in the
|
|
260
|
+
* snapshot) current until the returned stop is called, reading what the
|
|
261
|
+
* device does not hold. Nothing is read, and the snapshot leaves it empty,
|
|
262
|
+
* unless something tracks it: only the app badge needs it.
|
|
263
|
+
*/
|
|
264
|
+
trackUnreadPeople(): () => void {
|
|
265
|
+
return this.unreadPeople.track();
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
/** Channels with unread messages, from the counts and messages already held; no request. */
|
|
251
269
|
listUnreadInbox() {
|
|
252
270
|
return listUnreadInboxItems({
|
|
@@ -786,6 +804,7 @@ export class ChatController {
|
|
|
786
804
|
|
|
787
805
|
private emit(channelId?: string): void {
|
|
788
806
|
this.view.emit(channelId);
|
|
807
|
+
this.unreadPeople.sync();
|
|
789
808
|
}
|
|
790
809
|
|
|
791
810
|
private applyChannelState(state: ChatChannelState): void {
|
|
@@ -803,6 +822,7 @@ export class ChatController {
|
|
|
803
822
|
private stopRealtime(): void {
|
|
804
823
|
this.realtime.stopAll();
|
|
805
824
|
this.alerts.clear();
|
|
825
|
+
this.unreadPeople.clear();
|
|
806
826
|
}
|
|
807
827
|
|
|
808
828
|
private applySignedOutSession(): void {
|
|
@@ -39,9 +39,21 @@ export function isSameMessageRevision(current: ChatMessage, incoming: ChatMessag
|
|
|
39
39
|
&& attachmentKey(current) === attachmentKey(incoming);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Both lists are replaced on edits, reactions, pagination and send transitions.
|
|
43
|
+
// A draft save, upload progress or presence update changes neither. Keep the
|
|
44
|
+
// projection's identity then: subscribers must not redraw the entire transcript.
|
|
45
|
+
const visibleMessages = new WeakMap<ChannelRuntimeState, {
|
|
46
|
+
messages: ChatMessage[];
|
|
47
|
+
pending: ChatMessage[];
|
|
48
|
+
visible: ChatMessage[];
|
|
49
|
+
}>();
|
|
50
|
+
|
|
42
51
|
export function getVisibleMessages(channel: ChannelRuntimeState): ChatMessage[] {
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
const cached = visibleMessages.get(channel);
|
|
53
|
+
if (cached?.messages === channel.messages && cached.pending === channel.pendingMessages) return cached.visible;
|
|
54
|
+
const visible = [...channel.messages, ...channel.pendingMessages].sort(compareMessages);
|
|
55
|
+
visibleMessages.set(channel, { messages: channel.messages, pending: channel.pendingMessages, visible });
|
|
56
|
+
return visible;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
function mergeStoredMessages(channel: ChannelRuntimeState, messages: ChatMessage[]): ChatMessage[] {
|
|
@@ -44,6 +44,14 @@ export interface PersistedTranscript {
|
|
|
44
44
|
messages: ChatMessage[];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Who the unread messages of one channel are from, by account id. */
|
|
48
|
+
export interface ChannelUnreadPeople {
|
|
49
|
+
/** Everyone who sent an unread message there. */
|
|
50
|
+
senders: string[];
|
|
51
|
+
/** The ones among them with an unread message that mentions you. */
|
|
52
|
+
mentioners: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
export interface ChatControllerSnapshot {
|
|
48
56
|
channelId: string;
|
|
49
57
|
channels: ChatChannel[];
|
|
@@ -63,6 +71,13 @@ export interface ChatControllerSnapshot {
|
|
|
63
71
|
draftAttachments: ChatDraftAttachment[];
|
|
64
72
|
replyToId: string | null;
|
|
65
73
|
unreadMentionCount: number;
|
|
74
|
+
/**
|
|
75
|
+
* Who sent the unread messages of each channel with any, by channel id: what
|
|
76
|
+
* the app badge counts people from. Empty unless the badge asked for it
|
|
77
|
+
* ({@link ChatController.trackUnreadPeople}). A channel whose unread messages
|
|
78
|
+
* are not on this device yet lists only the ones that are, until they load.
|
|
79
|
+
*/
|
|
80
|
+
unreadPeople: Record<string, ChannelUnreadPeople>;
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
type ChatConnection = {
|