gloomberb 0.19.0 → 0.19.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -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/errors.ts +38 -0
- 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/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- 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 +30 -7
- 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 +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- 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/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- 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/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- 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/commodity-balances/client.ts +4 -1
- 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/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- 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 +18 -4
- 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/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- 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 +4 -2
- 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/jodi/client.ts +4 -1
- 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 +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- 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/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- 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 +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- 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/scanner/lock-note.ts +26 -3
- 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 +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- 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/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- 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/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -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/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- 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/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -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/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -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 +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- 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/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -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
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiClient, type ChatChannel, type ChatMessage } from "../../../../api-client";
|
|
2
|
-
import type { ChannelRuntimeState, ChannelUnreadPeople } from "./state";
|
|
2
|
+
import type { ChannelRuntimeState, ChannelUnreadPeople, UnreadMessageForYou } from "./state";
|
|
3
3
|
import { chatMessageMentionsUsername, normalizeChatUsername } from "./utils";
|
|
4
4
|
|
|
5
5
|
/** The most messages the server returns for one request. */
|
|
@@ -18,38 +18,73 @@ interface Reader {
|
|
|
18
18
|
username: string | null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/** What the app badge
|
|
22
|
-
interface UnreadFact {
|
|
23
|
-
|
|
21
|
+
/** What the app badge and the status bar need of one unread message. */
|
|
22
|
+
interface UnreadFact extends Omit<UnreadMessageForYou, "messageId"> {
|
|
23
|
+
messageId: string;
|
|
24
|
+
/** It mentions you, or replies to a message of yours. */
|
|
24
25
|
mentionsYou: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function sentAt(createdAt: string | null | undefined): number {
|
|
29
|
+
const time = createdAt ? Date.parse(createdAt) : Number.NaN;
|
|
30
|
+
return Number.isFinite(time) ? time : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether the message answers one of `reader`'s, by account or, from a server that leaves the id out, by username. */
|
|
34
|
+
function repliesTo(message: ChatMessage, reader: Reader): boolean {
|
|
35
|
+
const author = message.replyTo?.user;
|
|
36
|
+
if (!author) return false;
|
|
37
|
+
if (author.id) return author.id === reader.id;
|
|
38
|
+
return !!reader.username && normalizeChatUsername(author.username) === reader.username;
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
function unreadFact(message: ChatMessage, reader: Reader): UnreadFact {
|
|
28
42
|
return {
|
|
43
|
+
messageId: message.id,
|
|
29
44
|
senderId: message.user.id,
|
|
30
|
-
|
|
45
|
+
senderName: message.user.username?.trim() || message.user.displayName,
|
|
46
|
+
at: sentAt(message.createdAt),
|
|
47
|
+
mentionsYou: (!!reader.username && chatMessageMentionsUsername(message.content, reader.username))
|
|
48
|
+
|| repliesTo(message, reader),
|
|
31
49
|
};
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The people behind unread messages, and the newest one for you. In a DM or
|
|
54
|
+
* group chat (`everyMessageForYou`) every message is for you; elsewhere only
|
|
55
|
+
* the ones that mention or reply to you are.
|
|
56
|
+
*/
|
|
57
|
+
function peopleOf(facts: Iterable<UnreadFact>, everyMessageForYou: boolean): ChannelUnreadPeople {
|
|
35
58
|
const senders = new Set<string>();
|
|
36
59
|
const mentioners = new Set<string>();
|
|
60
|
+
let latestForYou: UnreadMessageForYou | null = null;
|
|
61
|
+
let forYouCount = 0;
|
|
37
62
|
for (const fact of facts) {
|
|
38
63
|
senders.add(fact.senderId);
|
|
39
64
|
if (fact.mentionsYou) mentioners.add(fact.senderId);
|
|
65
|
+
if (!everyMessageForYou && !fact.mentionsYou) continue;
|
|
66
|
+
forYouCount += 1;
|
|
67
|
+
if (!latestForYou || fact.at >= latestForYou.at) {
|
|
68
|
+
latestForYou = { messageId: fact.messageId, senderId: fact.senderId, senderName: fact.senderName, at: fact.at };
|
|
69
|
+
}
|
|
40
70
|
}
|
|
41
|
-
return { senders: [...senders], mentioners: [...mentioners] };
|
|
71
|
+
return { senders: [...senders], mentioners: [...mentioners], latestForYou, forYouCount };
|
|
42
72
|
}
|
|
43
73
|
|
|
44
74
|
/**
|
|
45
|
-
* The people behind unread messages: who sent them
|
|
46
|
-
* `reader
|
|
75
|
+
* The people behind unread messages: who sent them, who mentioned or replied
|
|
76
|
+
* to `reader`, and the newest message for them. Your own messages are never
|
|
77
|
+
* unread.
|
|
47
78
|
*/
|
|
48
79
|
export function summarizeUnreadMessages(
|
|
49
80
|
messages: readonly ChatMessage[],
|
|
50
81
|
reader: { id: string; username: string | null },
|
|
82
|
+
{ everyMessageForYou = false }: { everyMessageForYou?: boolean } = {},
|
|
51
83
|
): ChannelUnreadPeople {
|
|
52
|
-
return peopleOf(
|
|
84
|
+
return peopleOf(
|
|
85
|
+
messages.filter((message) => message.user.id !== reader.id).map((message) => unreadFact(message, reader)),
|
|
86
|
+
everyMessageForYou,
|
|
87
|
+
);
|
|
53
88
|
}
|
|
54
89
|
|
|
55
90
|
/** Messages read from the server for a channel whose unread messages are not all on this device. */
|
|
@@ -75,15 +110,16 @@ interface UnreadPeopleOptions {
|
|
|
75
110
|
}
|
|
76
111
|
|
|
77
112
|
/**
|
|
78
|
-
* Who each channel's unread messages are from, for the app badge
|
|
79
|
-
* keeps a count per channel and a read marker, not
|
|
80
|
-
* from the messages after the marker: the ones
|
|
81
|
-
* channel whose unread messages are not all
|
|
82
|
-
* read from the server once (then only what
|
|
83
|
-
* channel that has not loaded yet reports the
|
|
113
|
+
* Who each channel's unread messages are from, for the app badge and the
|
|
114
|
+
* status bar. The server keeps a count per channel and a read marker, not
|
|
115
|
+
* the senders, so they come from the messages after the marker: the ones
|
|
116
|
+
* this device holds, and for a channel whose unread messages are not all
|
|
117
|
+
* here, the newest 300 of the rest read from the server once (then only what
|
|
118
|
+
* is new). Nothing is guessed: a channel that has not loaded yet reports the
|
|
119
|
+
* unread messages it does hold.
|
|
84
120
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
121
|
+
* It does nothing, and makes no request, until something asks to
|
|
122
|
+
* {@link track} it: the app badge, and the status bar while signed in.
|
|
87
123
|
*/
|
|
88
124
|
export class ChatControllerUnreadPeople {
|
|
89
125
|
private readonly loaded = new Map<string, LoadedUnread>();
|
|
@@ -113,11 +149,21 @@ export class ChatControllerUnreadPeople {
|
|
|
113
149
|
const people: Record<string, ChannelUnreadPeople> = {};
|
|
114
150
|
for (const [channelId, state] of this.options.getChannelStates()) {
|
|
115
151
|
if (state.unreadCount <= 0) continue;
|
|
116
|
-
const
|
|
152
|
+
const channel = channels.get(channelId);
|
|
153
|
+
const conversation = channel?.kind === "direct" || channel?.kind === "group";
|
|
154
|
+
const known = peopleOf(this.knownFacts(channelId, state, reader).values(), conversation);
|
|
155
|
+
// In a DM or group every unread message is for you, held here or not.
|
|
156
|
+
if (conversation) known.forYouCount = state.unreadCount;
|
|
157
|
+
const otherPerson = directPerson(channel);
|
|
117
158
|
// Everything unread in a DM is from the one person on the other side.
|
|
118
159
|
people[channelId] = otherPerson
|
|
119
|
-
? {
|
|
120
|
-
|
|
160
|
+
? {
|
|
161
|
+
senders: [otherPerson.id],
|
|
162
|
+
mentioners: [],
|
|
163
|
+
latestForYou: known.latestForYou ?? newestFrom(otherPerson, state, channel),
|
|
164
|
+
forYouCount: known.forYouCount,
|
|
165
|
+
}
|
|
166
|
+
: known;
|
|
121
167
|
}
|
|
122
168
|
return people;
|
|
123
169
|
}
|
|
@@ -216,9 +262,29 @@ export class ChatControllerUnreadPeople {
|
|
|
216
262
|
}
|
|
217
263
|
}
|
|
218
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The newest unread message of a DM whose read marker is not on this device:
|
|
267
|
+
* unread messages are the newest ones, so it is the newest message held from
|
|
268
|
+
* the other person, or failing one, just them as of the conversation's last
|
|
269
|
+
* message.
|
|
270
|
+
*/
|
|
271
|
+
function newestFrom(
|
|
272
|
+
person: NonNullable<ChatChannel["dmUser"]>,
|
|
273
|
+
state: ChannelRuntimeState,
|
|
274
|
+
channel: ChatChannel | undefined,
|
|
275
|
+
): UnreadMessageForYou {
|
|
276
|
+
const held = state.messages.findLast((message) => message.user.id === person.id && !message.clientStatus);
|
|
277
|
+
return {
|
|
278
|
+
messageId: held?.id ?? null,
|
|
279
|
+
senderId: person.id,
|
|
280
|
+
senderName: person.username?.trim() || person.displayName,
|
|
281
|
+
at: Math.max(sentAt(held?.createdAt), sentAt(channel?.lastMessageAt)),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
219
285
|
/** The one other person in a direct message, when the channel says who. */
|
|
220
|
-
function directPerson(channel: ChatChannel | undefined):
|
|
221
|
-
return channel?.kind === "direct" ? channel.dmUser
|
|
286
|
+
function directPerson(channel: ChatChannel | undefined): NonNullable<ChatChannel["dmUser"]> | null {
|
|
287
|
+
return channel?.kind === "direct" ? channel.dmUser ?? null : null;
|
|
222
288
|
}
|
|
223
289
|
|
|
224
290
|
/**
|
|
@@ -20,9 +20,14 @@ export function chatMessageMentionsUsername(content: string, username: string):
|
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** The message
|
|
23
|
+
/** The message on one line, its images as `[image]` in front of the text. */
|
|
24
|
+
export function chatMessageLine(message: ChatMessage): string {
|
|
25
|
+
return chatTextWithImages(message.content, message.attachments?.length ?? 0).replace(/\s+/g, " ").trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The message as one short line. */
|
|
24
29
|
function formatMessageSnippet(message: ChatMessage): string {
|
|
25
|
-
const normalized =
|
|
30
|
+
const normalized = chatMessageLine(message);
|
|
26
31
|
return normalized.length > 72 ? `${normalized.slice(0, 69)}...` : normalized;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** How long a message for you shows in the status bar before it folds into the chip. */
|
|
2
|
+
const STATUS_PREVIEW_MS = 9_000;
|
|
3
|
+
|
|
4
|
+
/** A message for you, as the status bar shows it on arrival. */
|
|
5
|
+
export interface ChatStatusPreviewItem {
|
|
6
|
+
channelId: string;
|
|
7
|
+
messageId: string;
|
|
8
|
+
senderName: string;
|
|
9
|
+
/** It came in a DM or group chat; otherwise it mentions or replies to you. */
|
|
10
|
+
direct: boolean;
|
|
11
|
+
/** The message on one line. */
|
|
12
|
+
text: string;
|
|
13
|
+
/** When it was sent, in ms. */
|
|
14
|
+
at: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Whether the status bar has room for the item now. */
|
|
18
|
+
type PreviewRoom = (item: ChatStatusPreviewItem) => boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The message for you that the status bar shows on arrival, in place of the
|
|
22
|
+
* chat toast, for {@link STATUS_PREVIEW_MS}: then it folds into the chip that
|
|
23
|
+
* names who wants you. The newest message wins; nothing stacks. It shows only
|
|
24
|
+
* while a status bar with room for it is mounted ({@link attach}); otherwise
|
|
25
|
+
* the toast goes up as before.
|
|
26
|
+
*/
|
|
27
|
+
export class ChatStatusPreview {
|
|
28
|
+
private current: ChatStatusPreviewItem | null = null;
|
|
29
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
30
|
+
private readonly rooms = new Set<PreviewRoom>();
|
|
31
|
+
private readonly listeners = new Set<() => void>();
|
|
32
|
+
durationMs = STATUS_PREVIEW_MS;
|
|
33
|
+
|
|
34
|
+
/** A status bar that can show the preview when `room` says it fits, until the returned release. */
|
|
35
|
+
attach(room: PreviewRoom): () => void {
|
|
36
|
+
this.rooms.add(room);
|
|
37
|
+
return () => {
|
|
38
|
+
this.rooms.delete(room);
|
|
39
|
+
if (this.rooms.size === 0) this.dismiss();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Takes the message for the status bar when one has room for it, and shows
|
|
45
|
+
* it unless a newer one is showing (that one stays; this one counts in the
|
|
46
|
+
* chip). False when no status bar can: the caller raises its toast instead.
|
|
47
|
+
*/
|
|
48
|
+
offer(item: ChatStatusPreviewItem): boolean {
|
|
49
|
+
if (![...this.rooms].some((room) => room(item))) return false;
|
|
50
|
+
if (this.current && this.current.at > item.at) return true;
|
|
51
|
+
this.current = item;
|
|
52
|
+
if (this.timer) clearTimeout(this.timer);
|
|
53
|
+
this.timer = setTimeout(() => this.dismiss(), this.durationMs);
|
|
54
|
+
this.emit();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get = (): ChatStatusPreviewItem | null => this.current;
|
|
59
|
+
|
|
60
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
61
|
+
this.listeners.add(listener);
|
|
62
|
+
return () => {
|
|
63
|
+
this.listeners.delete(listener);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Folds the message into the chip now: opened, read, or no status bar left to show it. */
|
|
68
|
+
dismiss(): void {
|
|
69
|
+
if (this.timer) clearTimeout(this.timer);
|
|
70
|
+
this.timer = null;
|
|
71
|
+
if (!this.current) return;
|
|
72
|
+
this.current = null;
|
|
73
|
+
this.emit();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private emit(): void {
|
|
77
|
+
for (const listener of this.listeners) listener();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { Button } from "../../../components/ui/button";
|
|
2
|
+
import { Badge, ClippedText } from "../../../components/ui/display";
|
|
2
3
|
import { Icon } from "../../../components/ui/icon";
|
|
3
|
-
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import {
|
|
6
|
+
STATUS_WIDGET_COLUMNS,
|
|
7
|
+
useOtherStatusWidgetClaims,
|
|
8
|
+
useStatusWidgetClaim,
|
|
9
|
+
useStatusWidgetRoom,
|
|
10
|
+
} from "../../../components/layout/status-widget-space";
|
|
4
11
|
import { usePaneAppConfig } from "../../../state/app/context";
|
|
5
12
|
import { colors } from "../../../theme/colors";
|
|
6
|
-
import { Box, Span, Text,
|
|
13
|
+
import { Box, Span, Text, useUiCapabilities } from "../../../ui";
|
|
14
|
+
import { clipToDisplayWidth, displayWidth } from "../../../utils/format";
|
|
7
15
|
import { usePluginAppActions } from "../../runtime";
|
|
8
16
|
import { getNotificationLog, subscribeNotificationLog } from "../../../notifications/notification-log";
|
|
9
17
|
import { InlineAuthActions } from "../cloud/auth-actions";
|
|
@@ -11,11 +19,75 @@ import {
|
|
|
11
19
|
getPreferredChatOpenChannelId,
|
|
12
20
|
} from "./channels";
|
|
13
21
|
import { chatController, type ChatController } from "./controller";
|
|
22
|
+
import type { ChatStatusPreviewItem } from "./status-preview";
|
|
14
23
|
import { UNREAD_INBOX_TEMPLATE_ID } from "./unread-inbox";
|
|
15
|
-
import { unreadTotal } from "./unread-badge";
|
|
24
|
+
import { unreadForYou, unreadTotal } from "./unread-badge";
|
|
16
25
|
|
|
17
26
|
interface ChatStatusWidgetProps {
|
|
18
|
-
controller?: Pick<ChatController, "getSnapshot" | "refreshSession" | "subscribe">;
|
|
27
|
+
controller?: Pick<ChatController, "getSnapshot" | "refreshSession" | "subscribe" | "trackUnreadPeople" | "statusPreview">;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The widget's claim on the status bar's spare columns, for the chip and the message on arrival. */
|
|
31
|
+
const CLAIM_ID = "chat";
|
|
32
|
+
/** A name in the chip is cut to this many columns. */
|
|
33
|
+
const CHIP_NAME_COLUMNS = 12;
|
|
34
|
+
/** The message on arrival never takes more columns than this, and does not show in fewer than the least. */
|
|
35
|
+
const PREVIEW_MOST_COLUMNS = 48;
|
|
36
|
+
const PREVIEW_LEAST_COLUMNS = 12;
|
|
37
|
+
|
|
38
|
+
/** Who the chip names: the sender of the newest message for you, and whether it came in a DM or group chat. */
|
|
39
|
+
interface ChipWho {
|
|
40
|
+
senderName: string;
|
|
41
|
+
direct: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ChipFace {
|
|
45
|
+
icon?: "mail";
|
|
46
|
+
label: string;
|
|
47
|
+
/** Terminal cells, its padding included. */
|
|
48
|
+
columns: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The chip: an envelope for a DM or group message, `@` for a mention or a
|
|
53
|
+
* reply, then the sender's name and how many others want you, each left out
|
|
54
|
+
* when `name` or `more` says so.
|
|
55
|
+
*/
|
|
56
|
+
function chipFace(who: ChipWho, others: number, { name, more }: { name: boolean; more: boolean }): ChipFace {
|
|
57
|
+
const label = [
|
|
58
|
+
who.direct ? "" : "@",
|
|
59
|
+
name ? clipToDisplayWidth(who.senderName, CHIP_NAME_COLUMNS) : "",
|
|
60
|
+
more && others > 0 ? `+${others}` : "",
|
|
61
|
+
].filter(Boolean).join(" ");
|
|
62
|
+
// The glyph and its space (one cell wide in a terminal), then the label, inside one cell of padding each side.
|
|
63
|
+
const columns = (who.direct ? (label ? 2 : 1) : 0) + displayWidth(label) + 2;
|
|
64
|
+
return { ...(who.direct ? { icon: "mail" as const } : {}), label, columns };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The chip's faces from the fullest down: the name goes last, the count of others before it. */
|
|
68
|
+
const CHIP_FACES = [
|
|
69
|
+
{ name: true, more: true },
|
|
70
|
+
{ name: true, more: false },
|
|
71
|
+
{ name: false, more: true },
|
|
72
|
+
{ name: false, more: false },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How the chip and the message fit in `spare` columns beside the account
|
|
77
|
+
* name: the full chip and the message, then the chip alone, shorter and
|
|
78
|
+
* shorter, or nothing. One space goes after the chip; the message takes one
|
|
79
|
+
* before it and one more after, so it does not run into the account name.
|
|
80
|
+
*/
|
|
81
|
+
function fitChip(who: ChipWho | null, others: number, text: string | null, spare: number): { face: ChipFace; previewColumns: number } | null {
|
|
82
|
+
if (!who) return null;
|
|
83
|
+
for (const [index, parts] of CHIP_FACES.entries()) {
|
|
84
|
+
const face = chipFace(who, others, parts);
|
|
85
|
+
if (face.columns + 1 > spare) continue;
|
|
86
|
+
const room = Math.min(PREVIEW_MOST_COLUMNS, spare - face.columns - 3);
|
|
87
|
+
const previewColumns = text && index === 0 && room >= PREVIEW_LEAST_COLUMNS ? Math.min(room, displayWidth(text)) : 0;
|
|
88
|
+
return { face, previewColumns };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
19
91
|
}
|
|
20
92
|
|
|
21
93
|
function CloudStatusIcon() {
|
|
@@ -44,6 +116,7 @@ function CloudStatusIcon() {
|
|
|
44
116
|
|
|
45
117
|
export function ChatStatusWidget({ controller = chatController }: ChatStatusWidgetProps) {
|
|
46
118
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
119
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
47
120
|
const config = usePaneAppConfig();
|
|
48
121
|
const initialSnapshot = controller.getSnapshot();
|
|
49
122
|
const [username, setUsername] = useState<string | null>(initialSnapshot.user?.username ?? null);
|
|
@@ -51,7 +124,34 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
51
124
|
const [snapshot, setSnapshot] = useState(initialSnapshot);
|
|
52
125
|
// What the Unread list shows as unread: chat, then missed price, catalyst, economic release and team notices.
|
|
53
126
|
const notices = useSyncExternalStore(subscribeNotificationLog, getNotificationLog, getNotificationLog);
|
|
54
|
-
const
|
|
127
|
+
const preview = useSyncExternalStore(controller.statusPreview.subscribe, controller.statusPreview.get, controller.statusPreview.get);
|
|
128
|
+
const room = useStatusWidgetRoom();
|
|
129
|
+
const othersClaimed = useOtherStatusWidgetClaims(CLAIM_ID);
|
|
130
|
+
const signedIn = !!username || hasSavedSession;
|
|
131
|
+
|
|
132
|
+
// Who wants you: the people the app badge counts, the newest of them named in the chip.
|
|
133
|
+
const forYou = unreadForYou(snapshot);
|
|
134
|
+
const total = unreadTotal(snapshot, notices);
|
|
135
|
+
// A message on arrival shows until its conversation is read.
|
|
136
|
+
const unreadIn = (channelId: string) => snapshot.channelStates.find((state) => state.channelId === channelId)?.unreadCount ?? 0;
|
|
137
|
+
const arriving = preview && unreadIn(preview.channelId) > 0 ? preview : null;
|
|
138
|
+
const who: ChipWho | null = arriving ?? forYou.latest;
|
|
139
|
+
const others = Math.max(0, forYou.people - 1);
|
|
140
|
+
// The status bar holds back its usual columns for every widget's own text; the chip and the message are extra.
|
|
141
|
+
const spare = room - STATUS_WIDGET_COLUMNS - othersClaimed;
|
|
142
|
+
const fit = signedIn ? fitChip(who, others, arriving?.text ?? null, spare) : null;
|
|
143
|
+
// Chatter and missed notices: the rest, once the chip names who wants you.
|
|
144
|
+
const rest = fit ? Math.max(0, total - forYou.messages) : total;
|
|
145
|
+
const showsMessage = !!arriving && !!fit && fit.previewColumns > 0;
|
|
146
|
+
useStatusWidgetClaim(CLAIM_ID, fit ? fit.face.columns + 1 + (showsMessage ? fit.previewColumns + 2 : 0) : 0);
|
|
147
|
+
|
|
148
|
+
// The alerts ask, as a message arrives, whether the bar has room to show it; if not, its toast goes up.
|
|
149
|
+
const fits = useRef<(item: ChatStatusPreviewItem) => boolean>(() => false);
|
|
150
|
+
fits.current = (item) => (fitChip(item, others, item.text, spare)?.previewColumns ?? 0) > 0;
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!signedIn) return;
|
|
153
|
+
return controller.statusPreview.attach((item) => fits.current(item));
|
|
154
|
+
}, [controller, signedIn]);
|
|
55
155
|
|
|
56
156
|
const openChat = (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
57
157
|
event?.preventDefault?.();
|
|
@@ -66,6 +166,17 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
66
166
|
createPaneFromTemplate(UNREAD_INBOX_TEMPLATE_ID);
|
|
67
167
|
};
|
|
68
168
|
|
|
169
|
+
// The chip opens the conversation it names at the message.
|
|
170
|
+
const target = arriving ?? forYou.latest;
|
|
171
|
+
const openForYou = () => {
|
|
172
|
+
if (!target) return;
|
|
173
|
+
controller.statusPreview.dismiss();
|
|
174
|
+
createPaneFromTemplate("new-chat-pane", {
|
|
175
|
+
arg: target.channelId,
|
|
176
|
+
...(target.messageId ? { values: { messageId: target.messageId } } : {}),
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
69
180
|
useEffect(() => {
|
|
70
181
|
const unsubscribe = controller.subscribe((nextSnapshot) => {
|
|
71
182
|
setSnapshot(nextSnapshot);
|
|
@@ -76,17 +187,43 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
76
187
|
return unsubscribe;
|
|
77
188
|
}, [controller]);
|
|
78
189
|
|
|
190
|
+
// Who sent the unread messages is read only while someone is signed in.
|
|
191
|
+
useEffect(() => (signedIn ? controller.trackUnreadPeople() : undefined), [controller, signedIn]);
|
|
192
|
+
|
|
79
193
|
return (
|
|
80
194
|
<Box flexDirection="row" paddingRight={1}>
|
|
81
|
-
{!
|
|
195
|
+
{!signedIn ? (
|
|
82
196
|
<>
|
|
83
197
|
<CloudStatusIcon />
|
|
84
198
|
<InlineAuthActions showSignup={false} variant="plain" />
|
|
85
199
|
</>
|
|
86
200
|
) : (
|
|
87
201
|
<>
|
|
202
|
+
{fit && who ? (
|
|
203
|
+
<>
|
|
204
|
+
<Button
|
|
205
|
+
label={`Open the message from ${who.senderName}`}
|
|
206
|
+
variant="plain"
|
|
207
|
+
compact
|
|
208
|
+
flush
|
|
209
|
+
stopPropagation
|
|
210
|
+
onPress={openForYou}
|
|
211
|
+
>
|
|
212
|
+
<Box flexDirection="row" alignItems="center">
|
|
213
|
+
<Badge label={fit.face.label} icon={fit.face.icon} tone="warning" translate={false} />
|
|
214
|
+
{arriving && showsMessage ? (
|
|
215
|
+
// The terminal sizes the message to its cells; the desktop and the web let it shrink to its text and cut it there.
|
|
216
|
+
<Box {...(nativePaneChrome ? { maxWidth: fit.previewColumns + 1, flexShrink: 1 } : { width: fit.previewColumns + 1, flexShrink: 0 })} minWidth={0}>
|
|
217
|
+
<ClippedText text={arriving.text} width={fit.previewColumns} lead=" " fg={colors.text} cut={clipToDisplayWidth} />
|
|
218
|
+
</Box>
|
|
219
|
+
) : null}
|
|
220
|
+
</Box>
|
|
221
|
+
</Button>
|
|
222
|
+
<Text>{showsMessage ? " " : " "}</Text>
|
|
223
|
+
</>
|
|
224
|
+
) : null}
|
|
88
225
|
<Button label={username ? `Open chat as ${username}` : "Open chat"} variant="plain" compact stopPropagation onPress={openChat}>
|
|
89
|
-
<Text fg={
|
|
226
|
+
<Text fg={total > 0 ? colors.text : colors.textDim}>
|
|
90
227
|
<Span fg={colors.positive}>@</Span>
|
|
91
228
|
{username ? (
|
|
92
229
|
<>
|
|
@@ -96,15 +233,15 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
96
233
|
) : null}
|
|
97
234
|
</Text>
|
|
98
235
|
</Button>
|
|
99
|
-
{
|
|
236
|
+
{rest > 0 ? (
|
|
100
237
|
<Button
|
|
101
|
-
label={`Open ${
|
|
238
|
+
label={`Open ${rest} unread`}
|
|
102
239
|
variant="plain"
|
|
103
240
|
compact
|
|
104
241
|
stopPropagation
|
|
105
242
|
onPress={openUnread}
|
|
106
243
|
>
|
|
107
|
-
<Text fg={colors.
|
|
244
|
+
<Text fg={colors.textDim}>{` ${rest}`}</Text>
|
|
108
245
|
</Button>
|
|
109
246
|
) : null}
|
|
110
247
|
</>
|
|
@@ -23,22 +23,63 @@ function isPeopleConversation(channel: ChatChannel | undefined): boolean {
|
|
|
23
23
|
return channel?.kind === "direct" || channel?.kind === "group";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** The newest unread message for you, and where it is. */
|
|
27
|
+
interface LatestUnreadForYou {
|
|
28
|
+
channelId: string;
|
|
29
|
+
/** Null for a DM whose unread messages are not on this device: it opens at the conversation. */
|
|
30
|
+
messageId: string | null;
|
|
31
|
+
senderName: string;
|
|
32
|
+
/** It came in a DM or group chat; otherwise it mentions or replies to you. */
|
|
33
|
+
direct: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Who wants you: the people the app badge counts, and the newest of their messages. */
|
|
37
|
+
export interface UnreadForYou {
|
|
38
|
+
/** The person who wrote to you last; null when nobody wants you. */
|
|
39
|
+
latest: LatestUnreadForYou | null;
|
|
40
|
+
/** Everyone who sent you an unread DM or group message, or mentioned or replied to you, each once. */
|
|
41
|
+
people: number;
|
|
42
|
+
/** Their unread messages for you. */
|
|
43
|
+
messages: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const NOBODY: UnreadForYou = { latest: null, people: 0, messages: 0 };
|
|
47
|
+
|
|
26
48
|
/**
|
|
27
|
-
*
|
|
28
|
-
* unread
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* message ({@link unreadTotal}).
|
|
49
|
+
* Everyone who sent you an unread DM or group message, and everyone whose
|
|
50
|
+
* unread message in any other channel mentions or replies to you, each once
|
|
51
|
+
* however many messages they sent or where; with the newest of those
|
|
52
|
+
* messages and how many there are. Untagged channel messages are not for
|
|
53
|
+
* you. Nobody signed out.
|
|
33
54
|
*/
|
|
34
|
-
export function
|
|
35
|
-
if (!snapshot.user && !snapshot.hasSavedSession) return
|
|
55
|
+
export function unreadForYou(snapshot: BadgeSnapshot): UnreadForYou {
|
|
56
|
+
if (!snapshot.user && !snapshot.hasSavedSession) return NOBODY;
|
|
36
57
|
const channels = new Map(snapshot.channels.map((channel) => [channel.id, channel]));
|
|
37
58
|
const people = new Set<string>();
|
|
59
|
+
let latest: LatestUnreadForYou | null = null;
|
|
60
|
+
let latestAt = Number.NEGATIVE_INFINITY;
|
|
61
|
+
let messages = 0;
|
|
38
62
|
for (const [channelId, who] of Object.entries(snapshot.unreadPeople)) {
|
|
39
|
-
|
|
63
|
+
const direct = isPeopleConversation(channels.get(channelId));
|
|
64
|
+
for (const id of direct ? who.senders : who.mentioners) people.add(id);
|
|
65
|
+
messages += who.forYouCount;
|
|
66
|
+
const newest = who.latestForYou;
|
|
67
|
+
if (!newest || newest.at <= latestAt) continue;
|
|
68
|
+
latest = { channelId, messageId: newest.messageId, senderName: newest.senderName, direct };
|
|
69
|
+
latestAt = newest.at;
|
|
40
70
|
}
|
|
41
|
-
return people.size
|
|
71
|
+
return { latest, people: people.size, messages };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What the app badge counts: people, not messages ({@link unreadForYou}),
|
|
76
|
+
* then every missed notice not yet read. Untagged channel messages add
|
|
77
|
+
* nothing. Zero signed out. The status bar names the newest of those people
|
|
78
|
+
* and counts the rest of the unread messages ({@link unreadTotal}).
|
|
79
|
+
*/
|
|
80
|
+
export function unreadBadgeCount(snapshot: BadgeSnapshot, notices: readonly NotificationLogEntry[]): number {
|
|
81
|
+
if (!snapshot.user && !snapshot.hasSavedSession) return 0;
|
|
82
|
+
return unreadForYou(snapshot).people + unreadNotificationLogCount(notices);
|
|
42
83
|
}
|
|
43
84
|
|
|
44
85
|
interface UnreadBadgeOptions {
|