@gloom-sh/terminal 0.19.0 → 0.19.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/cli/commands/market.ts +35 -12
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +86 -20
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +6 -0
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/listing-arg.ts +57 -10
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +1 -1
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +2 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/screener.ts +8 -4
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/pane.tsx +5 -2
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +6 -1
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +27 -0
- package/src/types/headless.ts +6 -0
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/fundamentals.ts +36 -5
- package/src/version.ts +2 -2
|
@@ -5,6 +5,7 @@ import type { HeadlessPaneFreshness } from "../../../types/headless";
|
|
|
5
5
|
import type { TickerRecord } from "../../../types/ticker";
|
|
6
6
|
import { formatCompact } from "../../../utils/format";
|
|
7
7
|
import { compareSortValues, type SortPreference } from "../../../utils/sort-values";
|
|
8
|
+
import { typoDistance } from "../../../utils/typo-distance";
|
|
8
9
|
import { formatMove } from "../credit-boards/model";
|
|
9
10
|
|
|
10
11
|
export const CDS_PANE_ID = "cds";
|
|
@@ -20,7 +21,7 @@ const DECIMAL_TO_BP = 10_000;
|
|
|
20
21
|
|
|
21
22
|
export interface CdsTrade {
|
|
22
23
|
id: string;
|
|
23
|
-
/**
|
|
24
|
+
/** The issuer's best spelling on the tape, the same on every print of it (see `normalizeCdsTrades`). */
|
|
24
25
|
issuer: string;
|
|
25
26
|
/** Alias-collapsed grouping key. Internal only; never shown or sent. */
|
|
26
27
|
issuerKey: string;
|
|
@@ -31,8 +32,13 @@ export interface CdsTrade {
|
|
|
31
32
|
notionalCapped: boolean;
|
|
32
33
|
currency: string | null;
|
|
33
34
|
couponBp: number | null;
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Only what the report carried. Never derived from upfront or coupon, and
|
|
37
|
+
* empty when the field held points upfront (`pointsUpfront`).
|
|
38
|
+
*/
|
|
35
39
|
spreadBp: number | null;
|
|
40
|
+
/** The quote of a print whose spread field held points upfront, not bp. */
|
|
41
|
+
pointsUpfront: number | null;
|
|
36
42
|
/** Source inputs retained even when their notation cannot supply a bp rate. */
|
|
37
43
|
reportedSpread: number | null;
|
|
38
44
|
spreadNotation: string | null;
|
|
@@ -143,6 +149,66 @@ export function issuerGroupKey(name: string): string {
|
|
|
143
149
|
return (meaningful.length > 0 ? meaningful : tokens).join(" ") || name.trim().toLowerCase();
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/** Fewer letters than this are a symbol, not words of a name: `f` is Ford's ticker, not "F&G Annuities & Life". */
|
|
153
|
+
const MIN_NAME_LETTERS = 3;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The covered issuer a name stands for, by its words: the issuer of that very
|
|
157
|
+
* name, else one whose name holds those words in a row (`goodyear` is "The
|
|
158
|
+
* Goodyear Tire & Rubber Company"), one that starts with them first, then the
|
|
159
|
+
* shortest, so `ford` is Ford Motor Company and not Ford Motor Credit. Null
|
|
160
|
+
* when none does, and for fewer than three letters (`f`, `ge`), which are a
|
|
161
|
+
* symbol to look up.
|
|
162
|
+
*/
|
|
163
|
+
export function coveredIssuerMatch(name: string, covered: readonly string[]): string | null {
|
|
164
|
+
const wanted = issuerGroupKey(displayIssuerName(name));
|
|
165
|
+
if (wanted.replace(/[^\p{L}\p{N}]/gu, "").length < MIN_NAME_LETTERS) return null;
|
|
166
|
+
let best: { issuer: string; leading: boolean; words: number } | null = null;
|
|
167
|
+
for (const issuer of covered) {
|
|
168
|
+
const key = issuerGroupKey(displayIssuerName(issuer));
|
|
169
|
+
if (key === wanted) return displayIssuerName(issuer);
|
|
170
|
+
if (!` ${key} `.includes(` ${wanted} `)) continue;
|
|
171
|
+
const candidate = { issuer: displayIssuerName(issuer), leading: key.startsWith(`${wanted} `), words: key.split(" ").length };
|
|
172
|
+
if (!best
|
|
173
|
+
|| (candidate.leading !== best.leading ? candidate.leading
|
|
174
|
+
: candidate.words !== best.words ? candidate.words < best.words
|
|
175
|
+
: candidate.issuer.localeCompare(best.issuer) < 0)) best = candidate;
|
|
176
|
+
}
|
|
177
|
+
return best?.issuer ?? null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** How many covered issuers an unmatched name offers. */
|
|
181
|
+
const CLOSEST_ISSUERS = 3;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* How near a word is to one of a name's: a slip of the keys, or the same first
|
|
185
|
+
* letters (`goodyer`, `paramo`). Short words never count, or every name with
|
|
186
|
+
* "co" in it would be close.
|
|
187
|
+
*/
|
|
188
|
+
function wordCloseness(typed: string, word: string): number {
|
|
189
|
+
if (typed.length < 3 || word.length < 3) return 0;
|
|
190
|
+
if (typed.length >= 4 && typoDistance(typed, word) <= 1) return typed.length;
|
|
191
|
+
let shared = 0;
|
|
192
|
+
while (shared < Math.min(typed.length, word.length) && typed[shared] === word[shared]) shared += 1;
|
|
193
|
+
return shared >= 4 || (shared >= 3 && shared === typed.length) ? shared : 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The covered issuers closest to a name that matched none, closest first; none when nothing is near. */
|
|
197
|
+
export function closestCoveredIssuers(name: string, covered: readonly string[]): string[] {
|
|
198
|
+
const typed = issuerGroupKey(displayIssuerName(name)).split(" ").filter(Boolean);
|
|
199
|
+
const scored = new Map<string, number>();
|
|
200
|
+
for (const issuer of covered) {
|
|
201
|
+
const words = issuerGroupKey(displayIssuerName(issuer)).split(" ");
|
|
202
|
+
const score = typed.reduce((sum, word) => sum + Math.max(0, ...words.map((other) => wordCloseness(word, other))), 0);
|
|
203
|
+
const shown = displayIssuerName(issuer);
|
|
204
|
+
if (score > 0 && score > (scored.get(shown) ?? 0)) scored.set(shown, score);
|
|
205
|
+
}
|
|
206
|
+
return [...scored]
|
|
207
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
208
|
+
.slice(0, CLOSEST_ISSUERS)
|
|
209
|
+
.map(([issuer]) => issuer);
|
|
210
|
+
}
|
|
211
|
+
|
|
146
212
|
/**
|
|
147
213
|
* DTCC writes a comma inside a field as `;` ("CCO Holdings;LLC",
|
|
148
214
|
* "Amazon.com;Inc."), so a name gets its comma back before it is shown,
|
|
@@ -207,6 +273,37 @@ function couponToBasisPoints(fixedRate: number | null): number | null {
|
|
|
207
273
|
return fixedRate * DECIMAL_TO_BP;
|
|
208
274
|
}
|
|
209
275
|
|
|
276
|
+
/**
|
|
277
|
+
* How far a reported quote may sit from the points its upfront works out to
|
|
278
|
+
* and still be read as those points: accrued and rounding move the amount by
|
|
279
|
+
* about a tenth of a point.
|
|
280
|
+
*/
|
|
281
|
+
const POINTS_MATCH_PTS = 0.5;
|
|
282
|
+
const POINTS_MATCH_SHARE = 0.05;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Some desks key a distressed name's points upfront into the spread field and
|
|
286
|
+
* leave the coupon blank: Whirlpool's Dec 2028 contract printed `0.00065`,
|
|
287
|
+
* 6.5 read as bp, beside 319,861 upfront on 5M, which is 6.4 points. Without a
|
|
288
|
+
* coupon nothing makes the field a running spread, and when the amount over
|
|
289
|
+
* the notional reads as the same number in points, it is that many points.
|
|
290
|
+
* A real spread that low on a running coupon would need the upfront to land on
|
|
291
|
+
* the same number by chance, below 5bp.
|
|
292
|
+
*/
|
|
293
|
+
function pointsUpfrontOf(
|
|
294
|
+
spreadBp: number | null,
|
|
295
|
+
couponBp: number | null,
|
|
296
|
+
upfront: number | null,
|
|
297
|
+
notional: number | null,
|
|
298
|
+
): number | null {
|
|
299
|
+
if (couponBp != null || spreadBp == null || !(spreadBp > 0)) return null;
|
|
300
|
+
if (upfront == null || !Number.isFinite(upfront) || notional == null || !(notional > 0)) return null;
|
|
301
|
+
const impliedPts = (Math.abs(upfront) / notional) * 100;
|
|
302
|
+
const tolerance = Math.max(POINTS_MATCH_PTS, impliedPts * POINTS_MATCH_SHARE);
|
|
303
|
+
// The decimal the field holds carries float noise (0.001525 is 15.249999...).
|
|
304
|
+
return Math.abs(spreadBp - impliedPts) <= tolerance ? Math.round(spreadBp * 1e6) / 1e6 : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
210
307
|
/**
|
|
211
308
|
* When the trade was struck, not when the tape carried it. A lifecycle
|
|
212
309
|
* correction is disseminated long after the fact, so using the event time would
|
|
@@ -232,32 +329,46 @@ function issuerOf(trade: CloudCdsTradePayload): string | null {
|
|
|
232
329
|
return underlier && identifiesIssuer(underlier) ? displayIssuerName(underlier) : null;
|
|
233
330
|
}
|
|
234
331
|
|
|
332
|
+
/**
|
|
333
|
+
* The tape's prints as rows. One issuer reads as one name on every print:
|
|
334
|
+
* the tape spells Whirlpool three ways in a week ("WHIRLPOOL CORPORATION",
|
|
335
|
+
* "Whirlpool Corp", "Whirlpool Corporation"), and each row takes the spelling
|
|
336
|
+
* the issuer list shows for them all.
|
|
337
|
+
*/
|
|
235
338
|
export function normalizeCdsTrades(trades: readonly CloudCdsTradePayload[]): CdsTrade[] {
|
|
236
339
|
const rows: CdsTrade[] = [];
|
|
340
|
+
const names = new Map<string, string>();
|
|
237
341
|
for (const trade of trades) {
|
|
238
342
|
const eventAt = tapeTime(trade);
|
|
239
343
|
if (eventAt == null) continue;
|
|
240
344
|
const issuer = issuerOf(trade);
|
|
241
345
|
// Nothing names the reference entity, so the row would invent one.
|
|
242
346
|
if (!issuer) continue;
|
|
347
|
+
const issuerKey = issuerGroupKey(issuer);
|
|
348
|
+
const named = names.get(issuerKey);
|
|
349
|
+
names.set(issuerKey, named ? preferredIssuerName(named, issuer) : issuer);
|
|
350
|
+
const couponBp = couponToBasisPoints(trade.fixedRate);
|
|
351
|
+
const reportedBp = spreadToBasisPoints(trade.reportedSpread, trade.spreadNotation);
|
|
352
|
+
const pointsUpfront = pointsUpfrontOf(reportedBp, couponBp, trade.upfrontAmount, trade.notionalAmount);
|
|
243
353
|
rows.push({
|
|
244
354
|
id: trade.disseminationId,
|
|
245
355
|
issuer,
|
|
246
|
-
issuerKey
|
|
356
|
+
issuerKey,
|
|
247
357
|
eventAt,
|
|
248
358
|
maturity: trade.maturityDate ?? trade.expirationDate,
|
|
249
359
|
notional: trade.notionalAmount,
|
|
250
360
|
notionalCapped: trade.notionalCapped,
|
|
251
361
|
currency: trade.notionalCurrency,
|
|
252
|
-
couponBp
|
|
253
|
-
spreadBp:
|
|
362
|
+
couponBp,
|
|
363
|
+
spreadBp: pointsUpfront == null ? reportedBp : null,
|
|
364
|
+
pointsUpfront,
|
|
254
365
|
reportedSpread: trade.reportedSpread,
|
|
255
366
|
spreadNotation: trade.spreadNotation,
|
|
256
367
|
upfront: trade.upfrontAmount,
|
|
257
368
|
upfrontCurrency: trade.upfrontCurrency,
|
|
258
369
|
});
|
|
259
370
|
}
|
|
260
|
-
return rows;
|
|
371
|
+
return rows.map((row) => ({ ...row, issuer: names.get(row.issuerKey) ?? row.issuer }));
|
|
261
372
|
}
|
|
262
373
|
|
|
263
374
|
/**
|
|
@@ -405,13 +516,23 @@ export function cdsFreshness(lastTrade: number | string | null): HeadlessPaneFre
|
|
|
405
516
|
* What an empty view says: the window searched and, for one name, the
|
|
406
517
|
* reference entity the query became, with what was typed when that differs.
|
|
407
518
|
*/
|
|
408
|
-
export function noTradesMessage({ query, issuer, days }: {
|
|
519
|
+
export function noTradesMessage({ query, issuer, days, unmatched = null }: {
|
|
409
520
|
query: string | null;
|
|
410
521
|
issuer: string | null;
|
|
411
522
|
days: number;
|
|
523
|
+
/**
|
|
524
|
+
* A name that matched no covered issuer, once the tape has no trades and no
|
|
525
|
+
* line under it either: what the view says instead is that no issuer
|
|
526
|
+
* matches, with the covered ones closest to what was typed.
|
|
527
|
+
*/
|
|
528
|
+
unmatched?: { nearest: readonly string[] } | null;
|
|
412
529
|
}): string {
|
|
413
530
|
const window = `in the last ${days} days`;
|
|
414
531
|
if (!query) return `No single-name CDS trades ${window}.`;
|
|
532
|
+
if (unmatched) {
|
|
533
|
+
const nearest = unmatched.nearest.length ? ` Closest covered issuers: ${unmatched.nearest.join(", ")}.` : "";
|
|
534
|
+
return `No CDS issuer matches ${query}.${nearest}`;
|
|
535
|
+
}
|
|
415
536
|
const name = issuer && issuer.toUpperCase() !== query.toUpperCase() ? `${issuer} (${query})` : issuer ?? query;
|
|
416
537
|
return `No CDS trades for ${name} ${window}.`;
|
|
417
538
|
}
|
|
@@ -610,22 +731,25 @@ export function sortIssuers(
|
|
|
610
731
|
});
|
|
611
732
|
}
|
|
612
733
|
|
|
613
|
-
export type TradeColumnId = "time" | "maturity" | "notional" | "currency" | "coupon" | "spread" | "upfront";
|
|
734
|
+
export type TradeColumnId = "time" | "maturity" | "notional" | "currency" | "coupon" | "spread" | "points" | "upfront";
|
|
614
735
|
export type TradeColumn = DataTableColumn & { id: TradeColumnId };
|
|
615
736
|
export type TradeSortPreference = SortPreference<TradeColumnId>;
|
|
616
737
|
|
|
617
|
-
export const TRADE_SORT_COLUMN_IDS: readonly TradeColumnId[] = [
|
|
618
|
-
"time",
|
|
619
|
-
"maturity",
|
|
620
|
-
"notional",
|
|
621
|
-
"coupon",
|
|
622
|
-
"spread",
|
|
623
|
-
"upfront",
|
|
624
|
-
];
|
|
625
738
|
export const DEFAULT_TRADE_SORT: TradeSortPreference = { columnId: "time", direction: "desc" };
|
|
626
739
|
|
|
627
|
-
|
|
628
|
-
|
|
740
|
+
/** Whether any print was quoted in points upfront, which is when the trades show a Points column. */
|
|
741
|
+
export function hasPointsQuotes(trades: readonly Pick<CdsTrade, "pointsUpfront">[]): boolean {
|
|
742
|
+
return trades.some((trade) => trade.pointsUpfront != null);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** The columns `]` and `[` cycle the trades' sort through: the ones shown. */
|
|
746
|
+
export function tradeSortColumnIds(points: boolean): readonly TradeColumnId[] {
|
|
747
|
+
return ["time", "maturity", "notional", "coupon", "spread", ...(points ? ["points" as const] : []), "upfront"];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** A Points column sits beside Spread only when a print was quoted in points. */
|
|
751
|
+
export function buildTradeColumns(width: number, points = false): TradeColumn[] {
|
|
752
|
+
const maturityWidth = Math.max(10, Math.min(12, width - 59 - (points ? 9 : 0)));
|
|
629
753
|
return [
|
|
630
754
|
{ id: "time", label: "TIME UTC", width: 16, align: "left" },
|
|
631
755
|
{ id: "maturity", label: "MATURITY", width: maturityWidth, align: "left" },
|
|
@@ -633,6 +757,7 @@ export function buildTradeColumns(width: number): TradeColumn[] {
|
|
|
633
757
|
{ id: "currency", label: "CCY", width: 5, align: "left" },
|
|
634
758
|
{ id: "coupon", label: "COUPON", width: 9, align: "right" },
|
|
635
759
|
{ id: "spread", label: "SPREAD", width: 10, align: "right" },
|
|
760
|
+
...(points ? [{ id: "points" as const, label: "POINTS", width: 9, align: "right" as const }] : []),
|
|
636
761
|
{ id: "upfront", label: "ABS UPFRONT", width: 13, align: "right" },
|
|
637
762
|
];
|
|
638
763
|
}
|
|
@@ -651,6 +776,8 @@ function tradeSortValue(columnId: TradeColumnId, row: CdsTrade): string | number
|
|
|
651
776
|
return row.couponBp;
|
|
652
777
|
case "spread":
|
|
653
778
|
return row.spreadBp;
|
|
779
|
+
case "points":
|
|
780
|
+
return row.pointsUpfront;
|
|
654
781
|
case "upfront":
|
|
655
782
|
return row.upfront;
|
|
656
783
|
}
|
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
DEFAULT_ISSUER_SORT,
|
|
41
41
|
DEFAULT_TRADE_SORT,
|
|
42
42
|
ISSUER_SORT_COLUMN_IDS,
|
|
43
|
-
TRADE_SORT_COLUMN_IDS,
|
|
44
43
|
buildIssuerColumns,
|
|
45
44
|
buildTradeColumns,
|
|
46
45
|
cdsFreshness,
|
|
@@ -48,9 +47,11 @@ import {
|
|
|
48
47
|
formatEventTime,
|
|
49
48
|
formatMaturity,
|
|
50
49
|
formatNotional,
|
|
50
|
+
formatPoints,
|
|
51
51
|
formatQuoted,
|
|
52
52
|
formatQuotedMove,
|
|
53
53
|
formatUpfront,
|
|
54
|
+
hasPointsQuotes,
|
|
54
55
|
issuerListSort,
|
|
55
56
|
lastTradeOf,
|
|
56
57
|
noLevelMessage,
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
spreadChartPoints,
|
|
64
65
|
spreadFigures,
|
|
65
66
|
summarizeIssuers,
|
|
67
|
+
tradeSortColumnIds,
|
|
66
68
|
tradesForIssuer,
|
|
67
69
|
withSpreadLevels,
|
|
68
70
|
type CdsIssuerSummary,
|
|
@@ -138,6 +140,11 @@ function renderTradeCell(row: CdsTrade, column: TradeColumn, lineMaturity: strin
|
|
|
138
140
|
text: formatBp(row.spreadBp),
|
|
139
141
|
color: dimmed ?? (row.spreadBp == null ? colors.textDim : colors.textBright),
|
|
140
142
|
};
|
|
143
|
+
case "points":
|
|
144
|
+
return {
|
|
145
|
+
text: formatPoints(row.pointsUpfront),
|
|
146
|
+
color: dimmed ?? (row.pointsUpfront == null ? colors.textDim : colors.textBright),
|
|
147
|
+
};
|
|
141
148
|
case "upfront":
|
|
142
149
|
return {
|
|
143
150
|
text: formatUpfront(row),
|
|
@@ -175,7 +182,8 @@ function CdsTradeTable({
|
|
|
175
182
|
/** What an empty table says: the window and the name searched. */
|
|
176
183
|
emptyMessage: string;
|
|
177
184
|
}) {
|
|
178
|
-
const
|
|
185
|
+
const points = hasPointsQuotes(trades);
|
|
186
|
+
const columns = useMemo(() => buildTradeColumns(width, points), [points, width]);
|
|
179
187
|
const renderTradeRow = useCallback((trade: CdsTrade, column: TradeColumn) => (
|
|
180
188
|
renderTradeCell(trade, column, lineMaturity)
|
|
181
189
|
), [lineMaturity]);
|
|
@@ -276,7 +284,7 @@ function IssuerHeader({ issuer, line, trades, width, height, focused, loading, s
|
|
|
276
284
|
});
|
|
277
285
|
return (
|
|
278
286
|
<ChartTableHeader width={width} height={height} tableRows={trades.length} figures={figures}
|
|
279
|
-
tableColumns={buildTradeColumns(width)}
|
|
287
|
+
tableColumns={buildTradeColumns(width, hasPointsQuotes(trades))}
|
|
280
288
|
chart={points.length >= 2 || loading || why ? {
|
|
281
289
|
series,
|
|
282
290
|
formatValue: (value: number) => formatQuoted(unit, value),
|
|
@@ -388,12 +396,13 @@ export function CdsPane({
|
|
|
388
396
|
setDetailOpen(false);
|
|
389
397
|
}, [issuerQuery, issuers, selectedIssuerKey]);
|
|
390
398
|
|
|
399
|
+
const tradePoints = hasPointsQuotes(visibleTrades);
|
|
391
400
|
const cycleTradeSort = useCallback((step: 1 | -1) => {
|
|
392
401
|
setTradeSort((current) => {
|
|
393
|
-
const next = cycleSortPreference<TradeColumnId>(
|
|
402
|
+
const next = cycleSortPreference<TradeColumnId>(tradeSortColumnIds(tradePoints), current, step);
|
|
394
403
|
return { columnId: next.columnId ?? DEFAULT_TRADE_SORT.columnId, direction: next.direction };
|
|
395
404
|
});
|
|
396
|
-
}, []);
|
|
405
|
+
}, [tradePoints]);
|
|
397
406
|
const cycleIssuerSort = useCallback((step: 1 | -1) => {
|
|
398
407
|
setIssuerSort((current) => {
|
|
399
408
|
const next = cycleSortPreference<IssuerColumnId>(ISSUER_SORT_COLUMN_IDS, current, step);
|
|
@@ -451,6 +460,8 @@ export function CdsPane({
|
|
|
451
460
|
query: issuerQuery ? activity?.query ?? issuerQuery : selectedSummary?.issuer ?? null,
|
|
452
461
|
issuer: issuerQuery ? activity?.issuer ?? null : selectedSummary?.issuer ?? null,
|
|
453
462
|
days: activity?.days ?? CDS_TRADE_DAYS,
|
|
463
|
+
// A name the covered issuers do not have, once the tape has neither trades nor a line under it.
|
|
464
|
+
unmatched: issuerQuery && activity?.unmatched && !trades.length && history.data && !history.data.issuer ? activity.unmatched : null,
|
|
454
465
|
});
|
|
455
466
|
|
|
456
467
|
if (status === "loading" && !activity) {
|
|
@@ -21,6 +21,16 @@ function normalized(instrument: InstrumentRef): InstrumentRef {
|
|
|
21
21
|
const key = (instrument: InstrumentRef) => instrumentIdentityKey(normalized(instrument));
|
|
22
22
|
const publicKey = (instrument: InstrumentRef) => publicTickerKey(instrument.symbol, instrument.exchange);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* One listing however a series names it: the same symbol and broker contract, on the same venue
|
|
26
|
+
* or with the venue left out on either side (`AAPL` added beside a revenue line on `AAPL:NASDAQ`).
|
|
27
|
+
*/
|
|
28
|
+
function sameListing(left: InstrumentRef, right: InstrumentRef): boolean {
|
|
29
|
+
const a = normalized(left), b = normalized(right);
|
|
30
|
+
return key({ ...a, exchange: "" }) === key({ ...b, exchange: "" })
|
|
31
|
+
&& (!a.exchange || !b.exchange || canonicalExchange(a.exchange) === canonicalExchange(b.exchange));
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
/** Bootstrap old charts once; persisted IDs keep ownership through comparison collisions. */
|
|
25
35
|
export function resolveFollowSeriesIds(
|
|
26
36
|
spec: ChartSpec,
|
|
@@ -44,8 +54,10 @@ export function resolveFollowSeriesIds(
|
|
|
44
54
|
|| (next && securities.find(series => series.source.kind === "security" && publicKey(normalized(series.source.instrument)) === publicKey(next)))
|
|
45
55
|
|| securities[0];
|
|
46
56
|
if (!owner || owner.source.kind !== "security") return [];
|
|
47
|
-
const
|
|
48
|
-
|
|
57
|
+
const ownerInstrument = owner.source.instrument;
|
|
58
|
+
// Every field of the followed ticker moves with it (revenue and total debt); other tickers stay.
|
|
59
|
+
return securities.filter(series => series.source.kind === "security" && sameListing(series.source.instrument, ownerInstrument))
|
|
60
|
+
.map(series => series.id);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
/** Only the followed series move; other contracts and comparisons stay authored. */
|
|
@@ -10,7 +10,6 @@ import { parseTickerListInput } from "../../../tickers/list";
|
|
|
10
10
|
import { canonicalExchange, parsePublicTickerKey, publicTickerKey } from "../../../utils/exchanges";
|
|
11
11
|
import { tickerInstrumentLabel } from "../../../tickers/instrument-label";
|
|
12
12
|
import type { ChartSpec } from "../../../time-series/types";
|
|
13
|
-
import { isPriceOnlyMarketFieldId } from "../../../time-series/field-catalog";
|
|
14
13
|
import { ChartComposerPane, ChartComposerResearchTab } from "./pane";
|
|
15
14
|
import { DataCatalogPane } from "./data-catalog-pane";
|
|
16
15
|
import { listFieldSeries } from "./field-series";
|
|
@@ -484,9 +483,10 @@ const chartComposerTemplates: PaneTemplateDef[] = [
|
|
|
484
483
|
];
|
|
485
484
|
|
|
486
485
|
/**
|
|
487
|
-
* A
|
|
488
|
-
*
|
|
489
|
-
*
|
|
486
|
+
* A chart whose securities are all one ticker can follow a list: GP, GIP, and GF or GE of one
|
|
487
|
+
* ticker, whatever fields it shows (revenue beside total debt). Every series of that ticker moves
|
|
488
|
+
* to the selected one and keeps its field; FRED and other series stay as they are. A comparison
|
|
489
|
+
* of several tickers would move only one of them, and a FRED-only chart has no ticker to follow.
|
|
490
490
|
*/
|
|
491
491
|
function chartShowsOneSecurity(pane: PaneInstanceConfig): boolean {
|
|
492
492
|
const spec = parseChartSpec(pane.settings?.[CHART_SPEC_SETTING_KEY]);
|
|
@@ -495,7 +495,6 @@ function chartShowsOneSecurity(pane: PaneInstanceConfig): boolean {
|
|
|
495
495
|
const symbols = new Set<string>();
|
|
496
496
|
for (const series of spec.series) {
|
|
497
497
|
if (series.source.kind !== "security") continue;
|
|
498
|
-
if (!isPriceOnlyMarketFieldId(series.source.fieldId)) return false;
|
|
499
498
|
symbols.add(parsePublicTickerKey(series.source.instrument.symbol).symbol.toUpperCase());
|
|
500
499
|
}
|
|
501
500
|
return symbols.size === 1;
|
|
@@ -74,6 +74,7 @@ import { ChatControllerMessageLoading } from "./message-loading";
|
|
|
74
74
|
import { ChatMentionCandidates } from "./mention-candidates";
|
|
75
75
|
import { ChatControllerStorage } from "./storage";
|
|
76
76
|
import { listUnreadInboxItems } from "../unread-inbox";
|
|
77
|
+
import { ChatStatusPreview } from "../status-preview";
|
|
77
78
|
import type { ChatConversationList } from "../conversations";
|
|
78
79
|
import {
|
|
79
80
|
conversationActivityAt,
|
|
@@ -118,6 +119,8 @@ export class ChatController {
|
|
|
118
119
|
);
|
|
119
120
|
private notifyFn: (notification: AppNotificationRequest) => AppNotificationDelivery | void = () => {};
|
|
120
121
|
private openMessageFn: ((channelId: string, messageId: string) => void) | undefined;
|
|
122
|
+
/** A DM, mention or reply that shows in the status bar on arrival; the status bar widget draws it. */
|
|
123
|
+
readonly statusPreview = new ChatStatusPreview();
|
|
121
124
|
private readonly alerts = new ChatNotificationAlerts({
|
|
122
125
|
getAppActive: () => this.appActive,
|
|
123
126
|
ensureChannelState: (channelId) => this.ensureChannelState(channelId),
|
|
@@ -125,6 +128,7 @@ export class ChatController {
|
|
|
125
128
|
getChannel: (channelId) => this.channelCatalog.getChannels().find((channel) => channel.id === channelId),
|
|
126
129
|
getNotify: () => this.notifyFn,
|
|
127
130
|
getOpenMessage: () => this.openMessageFn,
|
|
131
|
+
statusPreview: this.statusPreview,
|
|
128
132
|
});
|
|
129
133
|
|
|
130
134
|
private readonly storage = new ChatControllerStorage({
|
|
@@ -271,7 +275,7 @@ export class ChatController {
|
|
|
271
275
|
* Keeps who sent each channel's unread messages (`unreadPeople` in the
|
|
272
276
|
* snapshot) current until the returned stop is called, reading what the
|
|
273
277
|
* device does not hold. Nothing is read, and the snapshot leaves it empty,
|
|
274
|
-
* unless something tracks it:
|
|
278
|
+
* unless something tracks it: the app badge and the status bar widget.
|
|
275
279
|
*/
|
|
276
280
|
trackUnreadPeople(): () => void {
|
|
277
281
|
return this.unreadPeople.track();
|
|
@@ -5,7 +5,9 @@ import type {
|
|
|
5
5
|
import { apiClient, type ChatChannel, type ChatMessage, type ChatNotification } from "../../../../api-client";
|
|
6
6
|
import type { ChannelRuntimeState, MergeMessagesOptions } from "./state";
|
|
7
7
|
import { formatChatPaneTitle } from "../channel-labels";
|
|
8
|
+
import type { ChatStatusPreview } from "../status-preview";
|
|
8
9
|
import {
|
|
10
|
+
chatMessageLine,
|
|
9
11
|
formatChannelToast,
|
|
10
12
|
formatMentionToast,
|
|
11
13
|
formatReplyToast,
|
|
@@ -28,6 +30,8 @@ interface ChatNotificationAlertsOptions {
|
|
|
28
30
|
getChannel: (channelId: string) => ChatChannel | undefined;
|
|
29
31
|
getNotify: () => Notify;
|
|
30
32
|
getOpenMessage: () => ((channelId: string, messageId: string) => void) | undefined;
|
|
33
|
+
/** Where a message for you shows on arrival instead of a toast, while the status bar has room. */
|
|
34
|
+
statusPreview: ChatStatusPreview;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/** Whether this window holds the message as read: its read marker is at or after it. */
|
|
@@ -53,9 +57,9 @@ function acknowledge(notification: ChatNotification, seen: boolean): void {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
|
-
* Decides, once per message, whether a chat notification
|
|
57
|
-
* a desktop banner. A message already read here or on
|
|
58
|
-
* quiet, whether it arrives live or again on reconnect.
|
|
60
|
+
* Decides, once per message, whether a chat notification shows in the status
|
|
61
|
+
* bar, as a toast or as a desktop banner. A message already read here or on
|
|
62
|
+
* another device stays quiet, whether it arrives live or again on reconnect.
|
|
59
63
|
*/
|
|
60
64
|
export class ChatNotificationAlerts {
|
|
61
65
|
private readonly notifiedMessageIds = new Set<string>();
|
|
@@ -94,6 +98,7 @@ export class ChatNotificationAlerts {
|
|
|
94
98
|
for (const timer of this.waiting.values()) clearTimeout(timer);
|
|
95
99
|
this.waiting.clear();
|
|
96
100
|
this.notifiedMessageIds.clear();
|
|
101
|
+
this.options.statusPreview.dismiss();
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/** Read here, or open in front of the reader: nothing to raise, and the server hears it was seen. */
|
|
@@ -124,8 +129,33 @@ export class ChatNotificationAlerts {
|
|
|
124
129
|
this.present(notification);
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
/**
|
|
133
|
+
* A DM or group message, a mention or a reply, while the app is in front:
|
|
134
|
+
* the status bar shows it in place of the toast when it has room, and the
|
|
135
|
+
* server hears it was seen, as for a toast. Channel chatter never shows there.
|
|
136
|
+
*/
|
|
137
|
+
private presentInStatusBar(notification: ChatNotification, channel: ChatChannel | undefined): boolean {
|
|
138
|
+
const direct = channel?.kind === "direct" || channel?.kind === "group";
|
|
139
|
+
if (!this.options.getAppActive() || (notification.type === "channel" && !direct)) return false;
|
|
140
|
+
const { message } = notification;
|
|
141
|
+
const sentAt = Date.parse(message.createdAt);
|
|
142
|
+
const shown = this.options.statusPreview.offer({
|
|
143
|
+
channelId: notification.channelId,
|
|
144
|
+
messageId: notification.messageId,
|
|
145
|
+
senderName: message.user.username?.trim() || message.user.displayName,
|
|
146
|
+
direct,
|
|
147
|
+
text: chatMessageLine(message),
|
|
148
|
+
at: Number.isFinite(sentAt) ? sentAt : Date.now(),
|
|
149
|
+
});
|
|
150
|
+
if (!shown) return false;
|
|
151
|
+
this.notifiedMessageIds.add(notification.messageId);
|
|
152
|
+
acknowledge(notification, true);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
private present(notification: ChatNotification): void {
|
|
128
157
|
const channel = this.options.getChannel(notification.channelId);
|
|
158
|
+
if (this.presentInStatusBar(notification, channel)) return;
|
|
129
159
|
const body = notification.type === "reply"
|
|
130
160
|
? formatReplyToast(notification.message)
|
|
131
161
|
: notification.type === "mention"
|
|
@@ -46,12 +46,34 @@ export interface PersistedTranscript {
|
|
|
46
46
|
messages: ChatMessage[];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** An unread message for you: who sent it, and when. */
|
|
50
|
+
export interface UnreadMessageForYou {
|
|
51
|
+
/** Null for a DM whose unread messages are not on this device: only the sender is known. */
|
|
52
|
+
messageId: string | null;
|
|
53
|
+
senderId: string;
|
|
54
|
+
/** The sender's username, or their name without one. */
|
|
55
|
+
senderName: string;
|
|
56
|
+
/** When it was sent, in ms; zero when not known. */
|
|
57
|
+
at: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
/** Who the unread messages of one channel are from, by account id. */
|
|
50
61
|
export interface ChannelUnreadPeople {
|
|
51
62
|
/** Everyone who sent an unread message there. */
|
|
52
63
|
senders: string[];
|
|
53
|
-
/** The ones among them with an unread message that mentions you. */
|
|
64
|
+
/** The ones among them with an unread message that mentions you or replies to you. */
|
|
54
65
|
mentioners: string[];
|
|
66
|
+
/**
|
|
67
|
+
* The newest unread message for you: in a DM or group chat the newest
|
|
68
|
+
* unread message, elsewhere the newest that mentions or replies to you.
|
|
69
|
+
* Null when none is known on this device.
|
|
70
|
+
*/
|
|
71
|
+
latestForYou: UnreadMessageForYou | null;
|
|
72
|
+
/**
|
|
73
|
+
* How many unread messages are for you: every one in a DM or group chat,
|
|
74
|
+
* the ones known to mention or reply to you elsewhere.
|
|
75
|
+
*/
|
|
76
|
+
forYouCount: number;
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
export interface ChatControllerSnapshot {
|
|
@@ -83,7 +105,8 @@ export interface ChatControllerSnapshot {
|
|
|
83
105
|
unreadMentionCount: number;
|
|
84
106
|
/**
|
|
85
107
|
* Who sent the unread messages of each channel with any, by channel id: what
|
|
86
|
-
* the app badge counts people from
|
|
108
|
+
* the app badge counts people from and the status bar names them from.
|
|
109
|
+
* Empty unless one of them asked for it
|
|
87
110
|
* ({@link ChatController.trackUnreadPeople}). A channel whose unread messages
|
|
88
111
|
* are not on this device yet lists only the ones that are, until they load.
|
|
89
112
|
*/
|