gloomberb 0.6.1 → 0.7.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/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { Box, ScrollBox, Span, Text, useUiCapabilities } from "../../ui";
|
|
2
|
-
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { memo, useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
3
3
|
import { useShortcut } from "../../react/input";
|
|
4
4
|
import { TextAttributes, type ScrollBoxRenderable, type TextareaRenderable } from "../../ui";
|
|
5
5
|
import type { GloomPlugin, PaneProps } from "../../types/plugin";
|
|
6
|
-
import { useAppDispatch, useAppSelector } from "../../state/app-context";
|
|
7
|
-
import { useInlineTickers } from "../../state/use-inline-tickers";
|
|
8
|
-
import { getMessageComposerBlockHeight, MessageComposer } from "../../components/ui";
|
|
6
|
+
import { useAppDispatch, useAppSelector, useAppStateRef, usePaneInstance, usePaneInstanceId } from "../../state/app-context";
|
|
7
|
+
import { useInlineTickers, type InlineTickerCatalogEntry } from "../../state/use-inline-tickers";
|
|
8
|
+
import { ExternalLinkText, getMessageComposerBlockHeight, MessageComposer } from "../../components/ui";
|
|
9
9
|
import { TickerBadgeText } from "../../components/ticker-badge-text";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { TickerBadge } from "../../components/ticker-badge";
|
|
11
|
+
import { blendHex, colors, hoverBg } from "../../theme/colors";
|
|
12
|
+
import { apiClient, type ChatChannel, type ChatMessage } from "../../utils/api-client";
|
|
12
13
|
import { formatTimeAgo } from "../../utils/format";
|
|
14
|
+
import { tokenizeInlineContent } from "../../utils/inline-content-tokenizer";
|
|
13
15
|
import { getSharedRegistry } from "../../plugins/registry";
|
|
14
16
|
import { usePluginAppActions } from "../../plugins/plugin-runtime";
|
|
17
|
+
import { setPaneSetting } from "../../pane-settings";
|
|
18
|
+
import { scheduleConfigSave } from "../../state/config-save-scheduler";
|
|
15
19
|
import { chatController, type ChatController } from "./chat-controller";
|
|
16
20
|
import { createGloomberbCloudCapabilities, createGloomberbCloudProvider } from "../../sources/gloomberb-cloud";
|
|
17
21
|
import { InlineAuthActions } from "./cloud-auth-actions";
|
|
@@ -22,16 +26,25 @@ interface ChatContentProps {
|
|
|
22
26
|
height: number;
|
|
23
27
|
focused: boolean;
|
|
24
28
|
close?: () => void;
|
|
29
|
+
channelId?: string;
|
|
30
|
+
onChannelChange?: (channelId: string) => void;
|
|
25
31
|
controller?: Pick<
|
|
26
32
|
ChatController,
|
|
27
33
|
| "attachView"
|
|
34
|
+
| "attachChannelView"
|
|
28
35
|
| "getSnapshot"
|
|
36
|
+
| "refreshChannels"
|
|
29
37
|
| "loadOlderMessages"
|
|
38
|
+
| "loadOlderChannelMessages"
|
|
30
39
|
| "refreshMessages"
|
|
40
|
+
| "refreshChannelMessages"
|
|
31
41
|
| "refreshSession"
|
|
32
42
|
| "send"
|
|
43
|
+
| "sendToChannel"
|
|
33
44
|
| "setDraft"
|
|
45
|
+
| "setChannelDraft"
|
|
34
46
|
| "setReplyToId"
|
|
47
|
+
| "setChannelReplyToId"
|
|
35
48
|
| "subscribe"
|
|
36
49
|
>;
|
|
37
50
|
}
|
|
@@ -42,10 +55,16 @@ interface ChatStatusWidgetProps {
|
|
|
42
55
|
|
|
43
56
|
const MESSAGE_GROUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
44
57
|
const CHAT_COMPOSER_MAX_ROWS = 5;
|
|
45
|
-
const NATIVE_CHAT_COMPOSER_TOP_GAP_PX = 12;
|
|
46
58
|
const MESSAGE_ACTION_WIDTH = 9;
|
|
47
59
|
const COMPOSER_ACTION_WIDTH = 10;
|
|
48
60
|
const MESSAGE_SELECTION_BOTTOM_INSET = 1;
|
|
61
|
+
const DESKTOP_MESSAGE_RIGHT_PADDING = 2;
|
|
62
|
+
const CHANNEL_SIDEBAR_MIN_WIDTH = 18;
|
|
63
|
+
const CHANNEL_SIDEBAR_MAX_WIDTH = 24;
|
|
64
|
+
const CHANNEL_SIDEBAR_BREAKPOINT = 72;
|
|
65
|
+
const DEFAULT_CHAT_CHANNEL_ID = "everyone";
|
|
66
|
+
const LAST_VISITED_CHAT_CHANNEL_KEY = "lastChatChannelId";
|
|
67
|
+
const CHAT_CHANNEL_MOUSE_HANDLED = "__gloomberbChatChannelHandled";
|
|
49
68
|
|
|
50
69
|
function isGroupedWithPrevious(messages: ChatMessage[], index: number) {
|
|
51
70
|
if (index === 0) return false;
|
|
@@ -117,9 +136,125 @@ function getMessageTopOffset(messages: ChatMessage[], index: number, width: numb
|
|
|
117
136
|
return offset;
|
|
118
137
|
}
|
|
119
138
|
|
|
120
|
-
function
|
|
139
|
+
function hasPixelScrollMetrics(scrollBox: ScrollBoxRenderable | null) {
|
|
140
|
+
return !!scrollBox?.scrollToPixels && typeof scrollBox.scrollHeightPx === "number" && !!scrollBox.viewportPx;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getScrollTop(scrollBox: ScrollBoxRenderable, preferPixels: boolean) {
|
|
144
|
+
return preferPixels && typeof scrollBox.scrollTopPx === "number" ? scrollBox.scrollTopPx : scrollBox.scrollTop;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getScrollHeight(scrollBox: ScrollBoxRenderable, preferPixels: boolean) {
|
|
148
|
+
return preferPixels && typeof scrollBox.scrollHeightPx === "number" ? scrollBox.scrollHeightPx : scrollBox.scrollHeight;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getViewportHeight(scrollBox: ScrollBoxRenderable, preferPixels: boolean) {
|
|
152
|
+
return preferPixels && scrollBox.viewportPx ? scrollBox.viewportPx.height : scrollBox.viewport?.height ?? 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function scrollToPosition(scrollBox: ScrollBoxRenderable, target: number, preferPixels: boolean) {
|
|
156
|
+
if (preferPixels && scrollBox.scrollToPixels) {
|
|
157
|
+
scrollBox.scrollToPixels(target);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
scrollBox.scrollTo(target);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scrollToBottom(scrollBox: ScrollBoxRenderable | null, preferPixels = false) {
|
|
121
164
|
if (!scrollBox) return;
|
|
122
|
-
|
|
165
|
+
const exactPixels = preferPixels && hasPixelScrollMetrics(scrollBox);
|
|
166
|
+
scrollToPosition(
|
|
167
|
+
scrollBox,
|
|
168
|
+
Math.max(0, getScrollHeight(scrollBox, exactPixels) - getViewportHeight(scrollBox, exactPixels)),
|
|
169
|
+
exactPixels,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function runAfterLayout(callback: () => void) {
|
|
174
|
+
if (typeof requestAnimationFrame === "function") {
|
|
175
|
+
requestAnimationFrame(callback);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
queueMicrotask(callback);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function getScrollTopForElementIntoView({
|
|
182
|
+
scrollTop,
|
|
183
|
+
viewportHeight,
|
|
184
|
+
elementTop,
|
|
185
|
+
elementHeight,
|
|
186
|
+
bottomInset = 0,
|
|
187
|
+
}: {
|
|
188
|
+
scrollTop: number;
|
|
189
|
+
viewportHeight: number;
|
|
190
|
+
elementTop: number;
|
|
191
|
+
elementHeight: number;
|
|
192
|
+
bottomInset?: number;
|
|
193
|
+
}) {
|
|
194
|
+
const visibleHeight = Math.max(viewportHeight - bottomInset, 1);
|
|
195
|
+
const elementBottom = elementTop + elementHeight;
|
|
196
|
+
if (elementTop < scrollTop || elementHeight >= visibleHeight) return elementTop;
|
|
197
|
+
if (elementBottom > scrollTop + visibleHeight) return elementBottom - visibleHeight;
|
|
198
|
+
return scrollTop;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function scrollElementIntoScrollBoxView(scrollBox: ScrollBoxRenderable | null, node: unknown) {
|
|
202
|
+
const nodeRect = (node as { getBoundingClientRect?: () => { x?: number; y?: number; top?: number; width?: number; height?: number } } | null)
|
|
203
|
+
?.getBoundingClientRect?.();
|
|
204
|
+
const scrollRect = scrollBox?.getBoundingClientRect?.() as { x?: number; y?: number; top?: number; width?: number; height?: number } | undefined;
|
|
205
|
+
if (
|
|
206
|
+
scrollBox &&
|
|
207
|
+
nodeRect &&
|
|
208
|
+
scrollRect &&
|
|
209
|
+
scrollBox.scrollToPixels &&
|
|
210
|
+
typeof scrollBox.scrollTopPx === "number" &&
|
|
211
|
+
scrollBox.viewportPx
|
|
212
|
+
) {
|
|
213
|
+
const nodeY = nodeRect.y ?? nodeRect.top ?? 0;
|
|
214
|
+
const scrollY = scrollRect.y ?? scrollRect.top ?? 0;
|
|
215
|
+
const elementTop = scrollBox.scrollTopPx + nodeY - scrollY;
|
|
216
|
+
const nextScrollTop = getScrollTopForElementIntoView({
|
|
217
|
+
scrollTop: scrollBox.scrollTopPx,
|
|
218
|
+
viewportHeight: scrollBox.viewportPx.height,
|
|
219
|
+
elementTop,
|
|
220
|
+
elementHeight: Math.max(nodeRect.height ?? 0, 1),
|
|
221
|
+
});
|
|
222
|
+
if (nextScrollTop !== scrollBox.scrollTopPx) {
|
|
223
|
+
scrollBox.scrollToPixels(nextScrollTop);
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const scrollIntoView = (node as { scrollIntoView?: (options?: unknown) => void } | null)?.scrollIntoView;
|
|
229
|
+
if (typeof scrollIntoView !== "function") return false;
|
|
230
|
+
scrollIntoView.call(node, { block: "nearest", inline: "nearest" });
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeChannelId(channelId: string | null | undefined) {
|
|
235
|
+
const trimmed = channelId?.trim();
|
|
236
|
+
return trimmed || DEFAULT_CHAT_CHANNEL_ID;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeShortcutChannelId(channelId: string | null | undefined) {
|
|
240
|
+
const trimmed = channelId?.trim().replace(/^#+/, "");
|
|
241
|
+
return normalizeChannelId(trimmed?.toLowerCase());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getLastVisitedChatChannelId(config: { pluginConfig: Record<string, Record<string, unknown>> }) {
|
|
245
|
+
return normalizeChannelId(config.pluginConfig["gloomberb-cloud"]?.[LAST_VISITED_CHAT_CHANNEL_KEY] as string | undefined);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatChannelLabel(channel: ChatChannel | undefined, fallbackId: string) {
|
|
249
|
+
return channel?.name?.trim() || fallbackId;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function truncateChannelLabel(label: string, width: number) {
|
|
253
|
+
if (width <= 0) return "";
|
|
254
|
+
if (label.length <= width) return label;
|
|
255
|
+
if (width <= 1) return label.slice(0, width);
|
|
256
|
+
if (width <= 3) return label.slice(0, width);
|
|
257
|
+
return `${label.slice(0, width - 3)}...`;
|
|
123
258
|
}
|
|
124
259
|
|
|
125
260
|
export function getSelectedMessageScrollTop({
|
|
@@ -213,15 +348,555 @@ function ChatActionChip({
|
|
|
213
348
|
);
|
|
214
349
|
}
|
|
215
350
|
|
|
351
|
+
interface ChatMessageRenderState {
|
|
352
|
+
isSelected: boolean;
|
|
353
|
+
isHovered: boolean;
|
|
354
|
+
grouped: boolean;
|
|
355
|
+
showReplyAction: boolean;
|
|
356
|
+
bgColor: string | undefined;
|
|
357
|
+
selectedTextColor: string;
|
|
358
|
+
replyMetaColor: string;
|
|
359
|
+
replyAuthorColor: string;
|
|
360
|
+
headerStatus: string;
|
|
361
|
+
headerStatusColor: string;
|
|
362
|
+
authorColor: string;
|
|
363
|
+
authorAttributes: number;
|
|
364
|
+
bodyColor: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getChatMessageRenderState({
|
|
368
|
+
msg,
|
|
369
|
+
index,
|
|
370
|
+
messages,
|
|
371
|
+
selectedIdx,
|
|
372
|
+
hoveredIdx,
|
|
373
|
+
canSend,
|
|
374
|
+
}: {
|
|
375
|
+
msg: ChatMessage;
|
|
376
|
+
index: number;
|
|
377
|
+
messages: ChatMessage[];
|
|
378
|
+
selectedIdx: number;
|
|
379
|
+
hoveredIdx: number | null;
|
|
380
|
+
canSend: boolean;
|
|
381
|
+
}): ChatMessageRenderState {
|
|
382
|
+
const isSelected = index === selectedIdx;
|
|
383
|
+
const isHovered = index === hoveredIdx && !isSelected;
|
|
384
|
+
const grouped = isGroupedWithPrevious(messages, index);
|
|
385
|
+
const isSending = msg.clientStatus === "sending";
|
|
386
|
+
const hasFailed = msg.clientStatus === "failed";
|
|
387
|
+
const selectedTextColor = hasFailed ? colors.negative : colors.selectedText;
|
|
388
|
+
const headerStatus = isSending ? "sending..." : hasFailed ? "failed" : formatTimeAgo(msg.createdAt);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
isSelected,
|
|
392
|
+
isHovered,
|
|
393
|
+
grouped,
|
|
394
|
+
showReplyAction: canSend && (isSelected || hoveredIdx === index),
|
|
395
|
+
bgColor: isSelected ? colors.selected : isHovered ? hoverBg() : undefined,
|
|
396
|
+
selectedTextColor,
|
|
397
|
+
replyMetaColor: isSelected ? selectedTextColor : colors.textMuted,
|
|
398
|
+
replyAuthorColor: isSelected ? selectedTextColor : colors.textDim,
|
|
399
|
+
headerStatus,
|
|
400
|
+
headerStatusColor: isSelected
|
|
401
|
+
? selectedTextColor
|
|
402
|
+
: isSending
|
|
403
|
+
? colors.textDim
|
|
404
|
+
: hasFailed
|
|
405
|
+
? colors.negative
|
|
406
|
+
: colors.textMuted,
|
|
407
|
+
authorColor: isSelected ? selectedTextColor : hasFailed ? colors.negative : colors.positive,
|
|
408
|
+
authorAttributes: (isSending ? TextAttributes.DIM : 0) | TextAttributes.BOLD,
|
|
409
|
+
bodyColor: isSelected ? selectedTextColor : hasFailed ? colors.negative : isSending ? colors.textDim : colors.text,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function ResponsiveTickerBadgeText({
|
|
414
|
+
text,
|
|
415
|
+
catalog,
|
|
416
|
+
textColor,
|
|
417
|
+
openTicker,
|
|
418
|
+
}: {
|
|
419
|
+
text: string;
|
|
420
|
+
catalog: Record<string, InlineTickerCatalogEntry>;
|
|
421
|
+
textColor: string;
|
|
422
|
+
openTicker: (symbol: string) => void;
|
|
423
|
+
}) {
|
|
424
|
+
const [hoveredSymbol, setHoveredSymbol] = useState<string | null>(null);
|
|
425
|
+
const tokens = useMemo(() => tokenizeInlineContent(text), [text]);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<Box
|
|
429
|
+
flexDirection="row"
|
|
430
|
+
flexWrap="wrap"
|
|
431
|
+
flexGrow={1}
|
|
432
|
+
style={{ minWidth: 0, width: "100%" }}
|
|
433
|
+
>
|
|
434
|
+
{tokens.map((token, index) => {
|
|
435
|
+
if (token.kind === "text") {
|
|
436
|
+
if (!token.value) return null;
|
|
437
|
+
return (
|
|
438
|
+
<Text
|
|
439
|
+
key={`text:${index}`}
|
|
440
|
+
fg={textColor}
|
|
441
|
+
wrapText
|
|
442
|
+
style={{
|
|
443
|
+
minWidth: 0,
|
|
444
|
+
whiteSpace: "pre-wrap",
|
|
445
|
+
overflowWrap: "anywhere",
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
448
|
+
{token.value}
|
|
449
|
+
</Text>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (token.kind === "link") {
|
|
454
|
+
return (
|
|
455
|
+
<ExternalLinkText
|
|
456
|
+
key={`link:${index}`}
|
|
457
|
+
url={token.url}
|
|
458
|
+
label={token.value}
|
|
459
|
+
color={textColor}
|
|
460
|
+
/>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const entry = catalog[token.symbol];
|
|
465
|
+
if (!entry || entry.status === "missing") {
|
|
466
|
+
return <Text key={`raw:${index}`} fg={textColor}>{token.value}</Text>;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<TickerBadge
|
|
471
|
+
key={`badge:${index}:${token.symbol}`}
|
|
472
|
+
symbol={token.symbol}
|
|
473
|
+
status={entry.status}
|
|
474
|
+
quote={entry.quote}
|
|
475
|
+
hovered={hoveredSymbol === token.symbol}
|
|
476
|
+
onHoverStart={() => setHoveredSymbol(token.symbol)}
|
|
477
|
+
onHoverEnd={() => {
|
|
478
|
+
setHoveredSymbol((current) => (current === token.symbol ? null : current));
|
|
479
|
+
}}
|
|
480
|
+
onOpen={openTicker}
|
|
481
|
+
/>
|
|
482
|
+
);
|
|
483
|
+
})}
|
|
484
|
+
</Box>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
interface ChatMessageBaseProps {
|
|
489
|
+
msg: ChatMessage;
|
|
490
|
+
index: number;
|
|
491
|
+
messages: ChatMessage[];
|
|
492
|
+
selectedIdx: number;
|
|
493
|
+
hoveredIdx: number | null;
|
|
494
|
+
canSend: boolean;
|
|
495
|
+
catalog: Record<string, InlineTickerCatalogEntry>;
|
|
496
|
+
openTicker: (symbol: string) => void;
|
|
497
|
+
beginReplyTo: (index: number, options?: { deferFocus?: boolean }) => void;
|
|
498
|
+
jumpToMessage: (messageId: string) => void;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
interface TerminalChatMessageProps extends ChatMessageBaseProps {
|
|
502
|
+
contentWidth: number;
|
|
503
|
+
messageBodyWidth: number;
|
|
504
|
+
setHoveredIdx: (updater: (current: number | null) => number | null) => void;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function TerminalChatMessage({
|
|
508
|
+
msg,
|
|
509
|
+
index,
|
|
510
|
+
messages,
|
|
511
|
+
selectedIdx,
|
|
512
|
+
hoveredIdx,
|
|
513
|
+
canSend,
|
|
514
|
+
contentWidth,
|
|
515
|
+
messageBodyWidth,
|
|
516
|
+
catalog,
|
|
517
|
+
openTicker,
|
|
518
|
+
beginReplyTo,
|
|
519
|
+
jumpToMessage,
|
|
520
|
+
setHoveredIdx,
|
|
521
|
+
}: TerminalChatMessageProps) {
|
|
522
|
+
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
523
|
+
const bodyLines = getMessageBodyLines(msg, contentWidth);
|
|
524
|
+
const showInlineReplyAction = !state.grouped && state.showReplyAction;
|
|
525
|
+
const showGroupedReplyAction = state.grouped && state.showReplyAction;
|
|
526
|
+
const setHovered = () => setHoveredIdx((current) => (current === index ? current : index));
|
|
527
|
+
const clearHovered = () => setHoveredIdx((current) => (current === index ? null : current));
|
|
528
|
+
const messageRowProps = {
|
|
529
|
+
width: contentWidth,
|
|
530
|
+
backgroundColor: state.bgColor,
|
|
531
|
+
onMouseMove: setHovered,
|
|
532
|
+
onMouseOut: clearHovered,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
return (
|
|
536
|
+
<Box key={msg.id} width={contentWidth} flexDirection="column">
|
|
537
|
+
{msg.replyTo && (
|
|
538
|
+
<Box
|
|
539
|
+
{...messageRowProps}
|
|
540
|
+
flexDirection="row"
|
|
541
|
+
height={1}
|
|
542
|
+
paddingLeft={2}
|
|
543
|
+
onMouseDown={() => {
|
|
544
|
+
if (msg.replyToId) jumpToMessage(msg.replyToId);
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<Text fg={state.replyMetaColor}>reply </Text>
|
|
548
|
+
<Text fg={state.replyAuthorColor}>{msg.replyTo.user.username}: </Text>
|
|
549
|
+
<Text fg={state.replyMetaColor}>
|
|
550
|
+
{formatInlinePreview(
|
|
551
|
+
msg.replyTo.content,
|
|
552
|
+
Math.max(messageBodyWidth - `reply ${msg.replyTo.user.username}: `.length, 0),
|
|
553
|
+
)}
|
|
554
|
+
</Text>
|
|
555
|
+
</Box>
|
|
556
|
+
)}
|
|
557
|
+
{!state.grouped && (
|
|
558
|
+
<Box
|
|
559
|
+
{...messageRowProps}
|
|
560
|
+
flexDirection="row"
|
|
561
|
+
height={1}
|
|
562
|
+
paddingLeft={1}
|
|
563
|
+
>
|
|
564
|
+
<Text fg={state.authorColor} attributes={state.authorAttributes}>
|
|
565
|
+
{msg.user.username ?? "anon"}
|
|
566
|
+
</Text>
|
|
567
|
+
<Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
|
|
568
|
+
{showInlineReplyAction && (
|
|
569
|
+
<>
|
|
570
|
+
<Text fg={state.headerStatusColor}> </Text>
|
|
571
|
+
<Box width={MESSAGE_ACTION_WIDTH} height={1}>
|
|
572
|
+
<ChatActionChip
|
|
573
|
+
label="Reply"
|
|
574
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
575
|
+
emphasized={state.isSelected}
|
|
576
|
+
onPress={() => beginReplyTo(index)}
|
|
577
|
+
/>
|
|
578
|
+
</Box>
|
|
579
|
+
</>
|
|
580
|
+
)}
|
|
581
|
+
</Box>
|
|
582
|
+
)}
|
|
583
|
+
{bodyLines.map((line, lineIndex) => (
|
|
584
|
+
<Box
|
|
585
|
+
key={`${msg.id}:body:${lineIndex}`}
|
|
586
|
+
{...messageRowProps}
|
|
587
|
+
paddingLeft={3}
|
|
588
|
+
height={1}
|
|
589
|
+
flexDirection="row"
|
|
590
|
+
position={state.grouped ? "relative" : undefined}
|
|
591
|
+
>
|
|
592
|
+
<Box width={messageBodyWidth} height={1}>
|
|
593
|
+
<TickerBadgeText
|
|
594
|
+
text={line}
|
|
595
|
+
lineWidth={messageBodyWidth}
|
|
596
|
+
catalog={catalog}
|
|
597
|
+
textColor={state.bodyColor}
|
|
598
|
+
openTicker={openTicker}
|
|
599
|
+
/>
|
|
600
|
+
</Box>
|
|
601
|
+
{lineIndex === 0 && showGroupedReplyAction && (
|
|
602
|
+
<Box position="absolute" top={0} right={0} width={MESSAGE_ACTION_WIDTH} height={1}>
|
|
603
|
+
<ChatActionChip
|
|
604
|
+
label="Reply"
|
|
605
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
606
|
+
emphasized={state.isSelected}
|
|
607
|
+
onPress={() => beginReplyTo(index)}
|
|
608
|
+
/>
|
|
609
|
+
</Box>
|
|
610
|
+
)}
|
|
611
|
+
</Box>
|
|
612
|
+
))}
|
|
613
|
+
</Box>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
618
|
+
msg,
|
|
619
|
+
index,
|
|
620
|
+
messages,
|
|
621
|
+
selectedIdx,
|
|
622
|
+
hoveredIdx,
|
|
623
|
+
canSend,
|
|
624
|
+
catalog,
|
|
625
|
+
openTicker,
|
|
626
|
+
beginReplyTo,
|
|
627
|
+
jumpToMessage,
|
|
628
|
+
registerMessageElement,
|
|
629
|
+
}: ChatMessageBaseProps & {
|
|
630
|
+
registerMessageElement: (messageId: string, node: unknown | null) => void;
|
|
631
|
+
}) {
|
|
632
|
+
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
633
|
+
const showInlineReplyAction = !state.grouped && canSend;
|
|
634
|
+
const showGroupedReplyAction = state.grouped && canSend;
|
|
635
|
+
const rowProps = {
|
|
636
|
+
width: "100%",
|
|
637
|
+
paddingRight: DESKTOP_MESSAGE_RIGHT_PADDING,
|
|
638
|
+
backgroundColor: state.bgColor,
|
|
639
|
+
"data-gloom-role": "chat-message-row",
|
|
640
|
+
"data-selected": state.isSelected ? "true" : "false",
|
|
641
|
+
style: { minWidth: 0 },
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<Box
|
|
646
|
+
key={msg.id}
|
|
647
|
+
ref={(node: unknown | null) => registerMessageElement(msg.id, node)}
|
|
648
|
+
width="100%"
|
|
649
|
+
flexDirection="column"
|
|
650
|
+
data-gloom-role="chat-message"
|
|
651
|
+
data-gloom-chat-message-id={msg.id}
|
|
652
|
+
data-selected={state.isSelected ? "true" : "false"}
|
|
653
|
+
style={{ "--chat-hover-bg": hoverBg(), minWidth: 0 }}
|
|
654
|
+
>
|
|
655
|
+
{msg.replyTo && (
|
|
656
|
+
<Box
|
|
657
|
+
{...rowProps}
|
|
658
|
+
flexDirection="row"
|
|
659
|
+
paddingLeft={2}
|
|
660
|
+
onMouseDown={() => {
|
|
661
|
+
if (msg.replyToId) jumpToMessage(msg.replyToId);
|
|
662
|
+
}}
|
|
663
|
+
style={{ minWidth: 0, alignItems: "flex-start", cursor: "pointer" }}
|
|
664
|
+
>
|
|
665
|
+
<Text
|
|
666
|
+
fg={state.replyMetaColor}
|
|
667
|
+
wrapText
|
|
668
|
+
style={{
|
|
669
|
+
minWidth: 0,
|
|
670
|
+
width: "100%",
|
|
671
|
+
display: "-webkit-box",
|
|
672
|
+
WebkitBoxOrient: "vertical",
|
|
673
|
+
WebkitLineClamp: 2,
|
|
674
|
+
overflow: "hidden",
|
|
675
|
+
textOverflow: "ellipsis",
|
|
676
|
+
whiteSpace: "normal",
|
|
677
|
+
overflowWrap: "anywhere",
|
|
678
|
+
}}
|
|
679
|
+
>
|
|
680
|
+
<Span fg={state.replyMetaColor}>reply </Span>
|
|
681
|
+
<Span fg={state.replyAuthorColor}>{msg.replyTo.user.username}: </Span>
|
|
682
|
+
<Span fg={state.replyMetaColor}>{normalizeInlinePreview(msg.replyTo.content)}</Span>
|
|
683
|
+
</Text>
|
|
684
|
+
</Box>
|
|
685
|
+
)}
|
|
686
|
+
{!state.grouped && (
|
|
687
|
+
<Box
|
|
688
|
+
{...rowProps}
|
|
689
|
+
flexDirection="row"
|
|
690
|
+
height={1}
|
|
691
|
+
paddingLeft={1}
|
|
692
|
+
>
|
|
693
|
+
<Text fg={state.authorColor} attributes={state.authorAttributes}>
|
|
694
|
+
{msg.user.username ?? "anon"}
|
|
695
|
+
</Text>
|
|
696
|
+
<Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
|
|
697
|
+
{showInlineReplyAction && (
|
|
698
|
+
<>
|
|
699
|
+
<Text fg={state.headerStatusColor}> </Text>
|
|
700
|
+
<Box
|
|
701
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
702
|
+
height={1}
|
|
703
|
+
data-gloom-role="chat-message-reply-action"
|
|
704
|
+
>
|
|
705
|
+
<ChatActionChip
|
|
706
|
+
label="Reply"
|
|
707
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
708
|
+
emphasized={state.isSelected}
|
|
709
|
+
onPress={() => beginReplyTo(index)}
|
|
710
|
+
/>
|
|
711
|
+
</Box>
|
|
712
|
+
</>
|
|
713
|
+
)}
|
|
714
|
+
</Box>
|
|
715
|
+
)}
|
|
716
|
+
<Box
|
|
717
|
+
{...rowProps}
|
|
718
|
+
paddingLeft={3}
|
|
719
|
+
flexDirection="row"
|
|
720
|
+
position={state.grouped ? "relative" : undefined}
|
|
721
|
+
style={{ minWidth: 0, alignItems: "flex-start" }}
|
|
722
|
+
>
|
|
723
|
+
<Box flexGrow={1} style={{ minWidth: 0 }}>
|
|
724
|
+
<ResponsiveTickerBadgeText
|
|
725
|
+
text={msg.content}
|
|
726
|
+
catalog={catalog}
|
|
727
|
+
textColor={state.bodyColor}
|
|
728
|
+
openTicker={openTicker}
|
|
729
|
+
/>
|
|
730
|
+
</Box>
|
|
731
|
+
{showGroupedReplyAction && (
|
|
732
|
+
<Box
|
|
733
|
+
position="absolute"
|
|
734
|
+
top={0}
|
|
735
|
+
right={0}
|
|
736
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
737
|
+
height={1}
|
|
738
|
+
data-gloom-role="chat-message-reply-action"
|
|
739
|
+
>
|
|
740
|
+
<ChatActionChip
|
|
741
|
+
label="Reply"
|
|
742
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
743
|
+
emphasized={state.isSelected}
|
|
744
|
+
onPress={() => beginReplyTo(index)}
|
|
745
|
+
/>
|
|
746
|
+
</Box>
|
|
747
|
+
)}
|
|
748
|
+
</Box>
|
|
749
|
+
</Box>
|
|
750
|
+
);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
function ChannelSidebar({
|
|
754
|
+
channels,
|
|
755
|
+
activeChannelId,
|
|
756
|
+
width,
|
|
757
|
+
height,
|
|
758
|
+
focused,
|
|
759
|
+
keyboardFocused,
|
|
760
|
+
loading,
|
|
761
|
+
onSelect,
|
|
762
|
+
onFocusRequest,
|
|
763
|
+
}: {
|
|
764
|
+
channels: ChatChannel[];
|
|
765
|
+
activeChannelId: string;
|
|
766
|
+
width: number;
|
|
767
|
+
height: number;
|
|
768
|
+
focused: boolean;
|
|
769
|
+
keyboardFocused: boolean;
|
|
770
|
+
loading: boolean;
|
|
771
|
+
onSelect?: (channelId: string) => void;
|
|
772
|
+
onFocusRequest?: () => void;
|
|
773
|
+
}) {
|
|
774
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
775
|
+
const borderWidth = nativePaneChrome ? 0 : width > 1 ? 1 : 0;
|
|
776
|
+
const listWidth = Math.max(width - borderWidth, 1);
|
|
777
|
+
const labelWidth = Math.max(listWidth - 3, 1);
|
|
778
|
+
const dividerColor = focused ? colors.borderFocused : colors.border;
|
|
779
|
+
const sidebarBg = keyboardFocused ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel;
|
|
780
|
+
const activeBg = keyboardFocused
|
|
781
|
+
? blendHex(colors.selected, colors.borderFocused, 0.32)
|
|
782
|
+
: blendHex(colors.panel, colors.selected, 0.35);
|
|
783
|
+
const [hoveredChannelId, setHoveredChannelId] = useState<string | null>(null);
|
|
784
|
+
const sidebarLayoutHeight = nativePaneChrome ? "100%" : height;
|
|
785
|
+
const nativeFillStyle = nativePaneChrome ? { minHeight: 0 } : undefined;
|
|
786
|
+
const sidebarBorder = borderWidth > 0
|
|
787
|
+
? (
|
|
788
|
+
<Box width={1} height={height} flexDirection="column">
|
|
789
|
+
{Array.from({ length: height }, (_, index) => (
|
|
790
|
+
<Text key={index} fg={dividerColor} selectable={false}>│</Text>
|
|
791
|
+
))}
|
|
792
|
+
</Box>
|
|
793
|
+
)
|
|
794
|
+
: null;
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
<Box
|
|
798
|
+
width={width}
|
|
799
|
+
height={sidebarLayoutHeight}
|
|
800
|
+
flexDirection="row"
|
|
801
|
+
position="relative"
|
|
802
|
+
style={nativeFillStyle}
|
|
803
|
+
>
|
|
804
|
+
<Box
|
|
805
|
+
width={listWidth}
|
|
806
|
+
height={sidebarLayoutHeight}
|
|
807
|
+
flexDirection="column"
|
|
808
|
+
backgroundColor={sidebarBg}
|
|
809
|
+
style={nativeFillStyle}
|
|
810
|
+
>
|
|
811
|
+
{channels.map((channel) => {
|
|
812
|
+
const active = channel.id === activeChannelId;
|
|
813
|
+
const hovered = hoveredChannelId === channel.id && !active;
|
|
814
|
+
const fg = active ? colors.selectedText : keyboardFocused ? colors.text : colors.textDim;
|
|
815
|
+
const bg = active ? activeBg : hovered ? hoverBg() : sidebarBg;
|
|
816
|
+
const label = formatChannelLabel(channel, channel.id);
|
|
817
|
+
const selectChannel = (event: any) => {
|
|
818
|
+
if (event) {
|
|
819
|
+
event.preventDefault?.();
|
|
820
|
+
event.stopPropagation?.();
|
|
821
|
+
if (event[CHAT_CHANNEL_MOUSE_HANDLED]) return;
|
|
822
|
+
event[CHAT_CHANNEL_MOUSE_HANDLED] = true;
|
|
823
|
+
}
|
|
824
|
+
onFocusRequest?.();
|
|
825
|
+
onSelect?.(channel.id);
|
|
826
|
+
};
|
|
827
|
+
return (
|
|
828
|
+
<Box
|
|
829
|
+
key={channel.id}
|
|
830
|
+
height={1}
|
|
831
|
+
width={listWidth}
|
|
832
|
+
flexDirection="row"
|
|
833
|
+
backgroundColor={bg}
|
|
834
|
+
onMouseDown={selectChannel}
|
|
835
|
+
onMouseMove={() => setHoveredChannelId((current) => (current === channel.id ? current : channel.id))}
|
|
836
|
+
onMouseOut={() => setHoveredChannelId((current) => (current === channel.id ? null : current))}
|
|
837
|
+
style={{ cursor: "pointer" }}
|
|
838
|
+
>
|
|
839
|
+
<Text fg={fg} selectable={false} onMouseDown={selectChannel}> </Text>
|
|
840
|
+
<Text fg={fg} selectable={false} onMouseDown={selectChannel}>{active ? "#" : " "}</Text>
|
|
841
|
+
<Text fg={fg} selectable={false} onMouseDown={selectChannel}>{truncateChannelLabel(label, labelWidth)}</Text>
|
|
842
|
+
<Box flexGrow={1} onMouseDown={selectChannel} />
|
|
843
|
+
</Box>
|
|
844
|
+
);
|
|
845
|
+
})}
|
|
846
|
+
<Box flexGrow={1} />
|
|
847
|
+
{loading && focused && (
|
|
848
|
+
<Box height={1} width={listWidth} flexDirection="row">
|
|
849
|
+
<Text fg={colors.textDim}> syncing</Text>
|
|
850
|
+
</Box>
|
|
851
|
+
)}
|
|
852
|
+
</Box>
|
|
853
|
+
{sidebarBorder}
|
|
854
|
+
{nativePaneChrome && (
|
|
855
|
+
<Box
|
|
856
|
+
position="absolute"
|
|
857
|
+
top={0}
|
|
858
|
+
right={0}
|
|
859
|
+
width={1}
|
|
860
|
+
height={sidebarLayoutHeight}
|
|
861
|
+
style={{
|
|
862
|
+
width: 1,
|
|
863
|
+
height: "100%",
|
|
864
|
+
backgroundColor: dividerColor,
|
|
865
|
+
pointerEvents: "none",
|
|
866
|
+
}}
|
|
867
|
+
/>
|
|
868
|
+
)}
|
|
869
|
+
</Box>
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
216
873
|
export function ChatContent({
|
|
217
874
|
width,
|
|
218
875
|
height,
|
|
219
876
|
focused,
|
|
220
877
|
close,
|
|
878
|
+
channelId: rawChannelId,
|
|
879
|
+
onChannelChange,
|
|
221
880
|
controller = chatController,
|
|
222
881
|
}: ChatContentProps) {
|
|
223
882
|
const dispatch = useAppDispatch();
|
|
224
|
-
const
|
|
883
|
+
const channelId = normalizeChannelId(rawChannelId);
|
|
884
|
+
const channelIdRef = useRef(channelId);
|
|
885
|
+
channelIdRef.current = channelId;
|
|
886
|
+
const initialSnapshot = controller.getSnapshot(channelId);
|
|
887
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
888
|
+
const [channels, setChannels] = useState<ChatChannel[]>(initialSnapshot.channels);
|
|
889
|
+
const [channelsLoading, setChannelsLoading] = useState(initialSnapshot.channelsLoading);
|
|
890
|
+
const showChannelSidebar = channels.length > 1 && width >= CHANNEL_SIDEBAR_BREAKPOINT && height >= 8;
|
|
891
|
+
const channelSidebarWidth = showChannelSidebar
|
|
892
|
+
? Math.min(CHANNEL_SIDEBAR_MAX_WIDTH, Math.max(CHANNEL_SIDEBAR_MIN_WIDTH, Math.floor(width * 0.24)))
|
|
893
|
+
: 0;
|
|
894
|
+
const compactChannelHeaderHeight = nativePaneChrome && !showChannelSidebar && channels.length > 1 ? 1 : 0;
|
|
895
|
+
const chatWidth = Math.max(width - channelSidebarWidth, 1);
|
|
896
|
+
const contentWidth = Math.max(chatWidth - 2, 1);
|
|
897
|
+
const composerPrefixWidth = nativePaneChrome ? 0 : 3;
|
|
898
|
+
const composerTextWidth = Math.max(contentWidth - composerPrefixWidth, 1);
|
|
899
|
+
const inputValueRef = useRef(initialSnapshot.draft);
|
|
225
900
|
const [messages, setMessages] = useState<ChatMessage[]>(initialSnapshot.messages);
|
|
226
901
|
const [hasSavedSession, setHasSavedSession] = useState(initialSnapshot.hasSavedSession);
|
|
227
902
|
const [user, setUser] = useState<{ id: string; username: string; emailVerified: boolean } | null>(initialSnapshot.user);
|
|
@@ -229,17 +904,22 @@ export function ChatContent({
|
|
|
229
904
|
const [loadingOlderMessages, setLoadingOlderMessages] = useState(initialSnapshot.loadingOlderMessages);
|
|
230
905
|
const [hasOlderMessages, setHasOlderMessages] = useState(initialSnapshot.hasOlderMessages);
|
|
231
906
|
const [inputFocused, setInputFocused] = useState(false);
|
|
232
|
-
const [
|
|
907
|
+
const [composerRows, setComposerRows] = useState(() => estimateComposerHeight(initialSnapshot.draft, composerTextWidth));
|
|
233
908
|
const [selectedIdx, setSelectedIdx] = useState(-1);
|
|
234
909
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
910
|
+
const [sidebarFocused, setSidebarFocusedState] = useState(false);
|
|
911
|
+
const sidebarFocusedRef = useRef(false);
|
|
912
|
+
const setSidebarFocused = useCallback((nextFocused: boolean) => {
|
|
913
|
+
sidebarFocusedRef.current = nextFocused;
|
|
914
|
+
setSidebarFocusedState((current) => (current === nextFocused ? current : nextFocused));
|
|
915
|
+
}, []);
|
|
235
916
|
const [followMessages, setFollowMessages] = useState(true);
|
|
236
|
-
const contentWidth = Math.max(width - 2, 1);
|
|
237
917
|
const [replyTo, setReplyTo] = useState<ChatMessage | null>(() => (
|
|
238
918
|
initialSnapshot.replyToId ? initialSnapshot.messages.find((message) => message.id === initialSnapshot.replyToId) ?? null : null
|
|
239
919
|
));
|
|
240
|
-
const { nativePaneChrome } = useUiCapabilities();
|
|
241
920
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
242
921
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
922
|
+
const messageElementsRef = useRef(new Map<string, unknown>());
|
|
243
923
|
const applyingExternalDraftRef = useRef(false);
|
|
244
924
|
const prependAnchorRef = useRef<{
|
|
245
925
|
oldestMessageId: string | null;
|
|
@@ -248,11 +928,8 @@ export function ChatContent({
|
|
|
248
928
|
selectedMessageId: string | null;
|
|
249
929
|
} | null>(null);
|
|
250
930
|
const canSend = !!user?.emailVerified;
|
|
251
|
-
const
|
|
931
|
+
const useDefaultControllerChannel = channelId === DEFAULT_CHAT_CHANNEL_ID && !onChannelChange;
|
|
252
932
|
const messageBodyWidth = Math.max(contentWidth - 4, 1);
|
|
253
|
-
const composerPrefixWidth = nativePaneChrome ? 0 : 3;
|
|
254
|
-
const composerTextWidth = Math.max(contentWidth - composerPrefixWidth, 1);
|
|
255
|
-
const composerRows = estimateComposerHeight(inputValue, composerTextWidth);
|
|
256
933
|
const composerHeight = canSend
|
|
257
934
|
? nativePaneChrome
|
|
258
935
|
? Math.min(CHAT_COMPOSER_MAX_ROWS + 1, Math.max(2, composerRows + 1))
|
|
@@ -260,20 +937,66 @@ export function ChatContent({
|
|
|
260
937
|
: 0;
|
|
261
938
|
const selectionActive = selectedIdx >= 0 && selectedIdx < messages.length;
|
|
262
939
|
const stickyTranscript = followMessages && !selectionActive;
|
|
940
|
+
const updateComposerRows = useCallback((draft: string) => {
|
|
941
|
+
const nextRows = estimateComposerHeight(draft, composerTextWidth);
|
|
942
|
+
setComposerRows((current) => (current === nextRows ? current : nextRows));
|
|
943
|
+
}, [composerTextWidth]);
|
|
263
944
|
|
|
264
945
|
useEffect(() => {
|
|
265
|
-
return controller.attachView();
|
|
946
|
+
return useDefaultControllerChannel ? controller.attachView() : controller.attachChannelView(channelId);
|
|
947
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
948
|
+
|
|
949
|
+
useEffect(() => {
|
|
950
|
+
updateComposerRows(inputValueRef.current);
|
|
951
|
+
}, [updateComposerRows]);
|
|
952
|
+
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
void controller.refreshChannels().catch(() => {});
|
|
955
|
+
void controller.refreshSession().catch(() => {});
|
|
266
956
|
}, [controller]);
|
|
267
957
|
|
|
268
958
|
useEffect(() => {
|
|
269
|
-
|
|
959
|
+
setSelectedIdx(-1);
|
|
960
|
+
setFollowMessages(true);
|
|
961
|
+
prependAnchorRef.current = null;
|
|
962
|
+
const snapshot = controller.getSnapshot(channelId);
|
|
963
|
+
setMessages(snapshot.messages);
|
|
964
|
+
setChannels(snapshot.channels);
|
|
965
|
+
setChannelsLoading(snapshot.channelsLoading);
|
|
966
|
+
setHasSavedSession(snapshot.hasSavedSession);
|
|
967
|
+
setUser(snapshot.user);
|
|
968
|
+
setLoading(snapshot.loading);
|
|
969
|
+
setLoadingOlderMessages(snapshot.loadingOlderMessages);
|
|
970
|
+
setHasOlderMessages(snapshot.hasOlderMessages);
|
|
971
|
+
inputValueRef.current = snapshot.draft;
|
|
972
|
+
updateComposerRows(snapshot.draft);
|
|
973
|
+
const textarea = inputRef.current;
|
|
974
|
+
if (textarea && textarea.editBuffer.getText() !== snapshot.draft) {
|
|
975
|
+
applyingExternalDraftRef.current = true;
|
|
976
|
+
textarea.setText(snapshot.draft);
|
|
977
|
+
applyingExternalDraftRef.current = false;
|
|
978
|
+
}
|
|
979
|
+
setReplyTo(snapshot.replyToId
|
|
980
|
+
? snapshot.messages.find((message) => message.id === snapshot.replyToId) ?? null
|
|
981
|
+
: null);
|
|
982
|
+
|
|
983
|
+
const unsubscribe = useDefaultControllerChannel
|
|
984
|
+
? controller.subscribe(handleSnapshot)
|
|
985
|
+
: controller.subscribe(channelId, handleSnapshot);
|
|
986
|
+
|
|
987
|
+
function handleSnapshot(snapshot: ReturnType<ChatController["getSnapshot"]>) {
|
|
270
988
|
setMessages(snapshot.messages);
|
|
989
|
+
setChannels(snapshot.channels);
|
|
990
|
+
setChannelsLoading(snapshot.channelsLoading);
|
|
271
991
|
setHasSavedSession(snapshot.hasSavedSession);
|
|
272
992
|
setUser(snapshot.user);
|
|
273
993
|
setLoading(snapshot.loading);
|
|
274
994
|
setLoadingOlderMessages(snapshot.loadingOlderMessages);
|
|
275
995
|
setHasOlderMessages(snapshot.hasOlderMessages);
|
|
276
|
-
|
|
996
|
+
if (inputValueRef.current !== snapshot.draft) {
|
|
997
|
+
inputValueRef.current = snapshot.draft;
|
|
998
|
+
updateComposerRows(snapshot.draft);
|
|
999
|
+
}
|
|
277
1000
|
const textarea = inputRef.current;
|
|
278
1001
|
if (textarea && textarea.editBuffer.getText() !== snapshot.draft) {
|
|
279
1002
|
applyingExternalDraftRef.current = true;
|
|
@@ -283,25 +1006,33 @@ export function ChatContent({
|
|
|
283
1006
|
setReplyTo(snapshot.replyToId
|
|
284
1007
|
? snapshot.messages.find((message) => message.id === snapshot.replyToId) ?? null
|
|
285
1008
|
: null);
|
|
286
|
-
}
|
|
1009
|
+
}
|
|
287
1010
|
|
|
288
|
-
void controller.
|
|
289
|
-
void controller.refreshMessages().catch(() => {});
|
|
1011
|
+
void (useDefaultControllerChannel ? controller.refreshMessages() : controller.refreshChannelMessages(channelId)).catch(() => {});
|
|
290
1012
|
return unsubscribe;
|
|
291
|
-
}, [controller]);
|
|
1013
|
+
}, [channelId, controller, updateComposerRows, useDefaultControllerChannel]);
|
|
292
1014
|
|
|
293
|
-
const
|
|
1015
|
+
const messageContents = useMemo(() => messages.map((message) => message.content), [messages]);
|
|
1016
|
+
const { catalog, openTicker } = useInlineTickers(messageContents);
|
|
294
1017
|
|
|
295
|
-
const inputValueRef = useRef(inputValue);
|
|
296
|
-
inputValueRef.current = inputValue;
|
|
297
1018
|
const replyToRef = useRef(replyTo);
|
|
298
1019
|
replyToRef.current = replyTo;
|
|
1020
|
+
const pendingJumpMessageIdRef = useRef<string | null>(null);
|
|
1021
|
+
|
|
1022
|
+
const registerMessageElement = useCallback((messageId: string, node: unknown | null) => {
|
|
1023
|
+
if (node) {
|
|
1024
|
+
messageElementsRef.current.set(messageId, node);
|
|
1025
|
+
} else {
|
|
1026
|
+
messageElementsRef.current.delete(messageId);
|
|
1027
|
+
}
|
|
1028
|
+
}, []);
|
|
299
1029
|
|
|
300
1030
|
const focusInput = useCallback(() => {
|
|
1031
|
+
setSidebarFocused(false);
|
|
301
1032
|
setInputFocused(true);
|
|
302
1033
|
dispatch({ type: "SET_INPUT_CAPTURED", captured: true });
|
|
303
|
-
inputRef.current?.focus();
|
|
304
|
-
}, [dispatch]);
|
|
1034
|
+
inputRef.current?.focus?.();
|
|
1035
|
+
}, [dispatch, setSidebarFocused]);
|
|
305
1036
|
|
|
306
1037
|
const focusComposer = useCallback(() => {
|
|
307
1038
|
setSelectedIdx(-1);
|
|
@@ -316,8 +1047,12 @@ export function ChatContent({
|
|
|
316
1047
|
|
|
317
1048
|
const clearReplyTarget = useCallback(() => {
|
|
318
1049
|
setReplyTo(null);
|
|
319
|
-
|
|
320
|
-
|
|
1050
|
+
if (useDefaultControllerChannel) {
|
|
1051
|
+
controller.setReplyToId(null);
|
|
1052
|
+
} else {
|
|
1053
|
+
controller.setChannelReplyToId(channelId, null);
|
|
1054
|
+
}
|
|
1055
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
321
1056
|
|
|
322
1057
|
const beginReplyTo = useCallback((index: number, options?: { deferFocus?: boolean }) => {
|
|
323
1058
|
if (!canSend || index < 0 || index >= messages.length) return;
|
|
@@ -326,13 +1061,17 @@ export function ChatContent({
|
|
|
326
1061
|
setSelectedIdx(index);
|
|
327
1062
|
setFollowMessages(index === messages.length - 1);
|
|
328
1063
|
setReplyTo(nextReplyTo);
|
|
329
|
-
|
|
1064
|
+
if (useDefaultControllerChannel) {
|
|
1065
|
+
controller.setReplyToId(nextReplyTo.id);
|
|
1066
|
+
} else {
|
|
1067
|
+
controller.setChannelReplyToId(channelId, nextReplyTo.id);
|
|
1068
|
+
}
|
|
330
1069
|
if (options?.deferFocus) {
|
|
331
1070
|
queueMicrotask(() => focusInput());
|
|
332
1071
|
} else {
|
|
333
1072
|
focusInput();
|
|
334
1073
|
}
|
|
335
|
-
}, [canSend, controller, focusInput, messages]);
|
|
1074
|
+
}, [canSend, channelId, controller, focusInput, messages, useDefaultControllerChannel]);
|
|
336
1075
|
|
|
337
1076
|
const returnToComposer = useCallback(() => {
|
|
338
1077
|
setSelectedIdx(-1);
|
|
@@ -375,26 +1114,34 @@ export function ChatContent({
|
|
|
375
1114
|
const sendMessage = useCallback(() => {
|
|
376
1115
|
const content = inputValueRef.current.trim();
|
|
377
1116
|
if (!content) return;
|
|
378
|
-
|
|
1117
|
+
if (useDefaultControllerChannel) {
|
|
1118
|
+
controller.send(content, replyToRef.current?.id);
|
|
1119
|
+
} else {
|
|
1120
|
+
controller.sendToChannel(channelId, content, replyToRef.current?.id);
|
|
1121
|
+
}
|
|
379
1122
|
setSelectedIdx(-1);
|
|
380
1123
|
setFollowMessages(true);
|
|
381
|
-
}, [controller]);
|
|
1124
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
382
1125
|
|
|
383
1126
|
const requestOlderMessages = useCallback(() => {
|
|
384
1127
|
const scrollBox = scrollRef.current;
|
|
385
1128
|
if (!scrollBox || loadingOlderMessages || !hasOlderMessages || messages.length === 0) return;
|
|
1129
|
+
const exactPixels = nativePaneChrome === true && hasPixelScrollMetrics(scrollBox);
|
|
386
1130
|
|
|
387
1131
|
prependAnchorRef.current = {
|
|
388
1132
|
oldestMessageId: messages[0]?.id ?? null,
|
|
389
|
-
scrollHeight: scrollBox
|
|
390
|
-
scrollTop: scrollBox
|
|
1133
|
+
scrollHeight: getScrollHeight(scrollBox, exactPixels),
|
|
1134
|
+
scrollTop: getScrollTop(scrollBox, exactPixels),
|
|
391
1135
|
selectedMessageId: selectedIdx >= 0 ? messages[selectedIdx]?.id ?? null : null,
|
|
392
1136
|
};
|
|
393
1137
|
setFollowMessages(false);
|
|
394
|
-
|
|
1138
|
+
const request = useDefaultControllerChannel
|
|
1139
|
+
? controller.loadOlderMessages()
|
|
1140
|
+
: controller.loadOlderChannelMessages(channelId);
|
|
1141
|
+
void request.catch(() => {
|
|
395
1142
|
prependAnchorRef.current = null;
|
|
396
1143
|
});
|
|
397
|
-
}, [controller, hasOlderMessages, loadingOlderMessages, messages, selectedIdx]);
|
|
1144
|
+
}, [channelId, controller, hasOlderMessages, loadingOlderMessages, messages, nativePaneChrome, selectedIdx, useDefaultControllerChannel]);
|
|
398
1145
|
|
|
399
1146
|
const requestOlderMessagesIfNeeded = useCallback(() => {
|
|
400
1147
|
const scrollBox = scrollRef.current;
|
|
@@ -402,6 +1149,110 @@ export function ChatContent({
|
|
|
402
1149
|
requestOlderMessages();
|
|
403
1150
|
}, [requestOlderMessages]);
|
|
404
1151
|
|
|
1152
|
+
const scrollToLoadedMessage = useCallback((messageId: string) => {
|
|
1153
|
+
const targetIndex = messages.findIndex((message) => message.id === messageId);
|
|
1154
|
+
if (targetIndex < 0) return false;
|
|
1155
|
+
|
|
1156
|
+
setSelectedIdx(targetIndex);
|
|
1157
|
+
setFollowMessages(false);
|
|
1158
|
+
prependAnchorRef.current = null;
|
|
1159
|
+
runAfterLayout(() => {
|
|
1160
|
+
if (nativePaneChrome && scrollElementIntoScrollBoxView(scrollRef.current, messageElementsRef.current.get(messageId))) return;
|
|
1161
|
+
const scrollBox = scrollRef.current;
|
|
1162
|
+
if (!scrollBox) return;
|
|
1163
|
+
scrollBox.scrollTo(getMessageTopOffset(messages, targetIndex, contentWidth));
|
|
1164
|
+
});
|
|
1165
|
+
return true;
|
|
1166
|
+
}, [contentWidth, messages, nativePaneChrome]);
|
|
1167
|
+
|
|
1168
|
+
const jumpToMessage = useCallback((messageId: string) => {
|
|
1169
|
+
if (scrollToLoadedMessage(messageId)) return;
|
|
1170
|
+
pendingJumpMessageIdRef.current = messageId;
|
|
1171
|
+
if (hasOlderMessages && !loadingOlderMessages) {
|
|
1172
|
+
requestOlderMessages();
|
|
1173
|
+
} else {
|
|
1174
|
+
pendingJumpMessageIdRef.current = null;
|
|
1175
|
+
}
|
|
1176
|
+
}, [hasOlderMessages, loadingOlderMessages, requestOlderMessages, scrollToLoadedMessage]);
|
|
1177
|
+
|
|
1178
|
+
useEffect(() => {
|
|
1179
|
+
const pendingId = pendingJumpMessageIdRef.current;
|
|
1180
|
+
if (!pendingId) return;
|
|
1181
|
+
if (scrollToLoadedMessage(pendingId)) {
|
|
1182
|
+
pendingJumpMessageIdRef.current = null;
|
|
1183
|
+
prependAnchorRef.current = null;
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (hasOlderMessages && !loadingOlderMessages) {
|
|
1187
|
+
requestOlderMessages();
|
|
1188
|
+
} else {
|
|
1189
|
+
pendingJumpMessageIdRef.current = null;
|
|
1190
|
+
}
|
|
1191
|
+
}, [hasOlderMessages, loadingOlderMessages, messages, requestOlderMessages, scrollToLoadedMessage]);
|
|
1192
|
+
|
|
1193
|
+
const changeChannel = useCallback((nextChannelId: string) => {
|
|
1194
|
+
const normalized = normalizeChannelId(nextChannelId);
|
|
1195
|
+
if (normalized === channelIdRef.current) return;
|
|
1196
|
+
channelIdRef.current = normalized;
|
|
1197
|
+
setSelectedIdx(-1);
|
|
1198
|
+
setFollowMessages(true);
|
|
1199
|
+
onChannelChange?.(normalized);
|
|
1200
|
+
}, [onChannelChange]);
|
|
1201
|
+
|
|
1202
|
+
useEffect(() => {
|
|
1203
|
+
if (!onChannelChange || channelsLoading || channels.length === 0) return;
|
|
1204
|
+
if (channels.some((channel) => channel.id === channelId)) return;
|
|
1205
|
+
const fallbackChannelId = channels.find((channel) => channel.id === DEFAULT_CHAT_CHANNEL_ID)?.id
|
|
1206
|
+
?? channels[0]?.id;
|
|
1207
|
+
if (fallbackChannelId) {
|
|
1208
|
+
changeChannel(fallbackChannelId);
|
|
1209
|
+
}
|
|
1210
|
+
}, [changeChannel, channelId, channels, channelsLoading, onChannelChange]);
|
|
1211
|
+
|
|
1212
|
+
const cycleChannel = useCallback((direction: 1 | -1) => {
|
|
1213
|
+
if (channels.length <= 1 || !onChannelChange) return false;
|
|
1214
|
+
const currentIndex = Math.max(0, channels.findIndex((channel) => channel.id === channelIdRef.current));
|
|
1215
|
+
const nextIndex = (currentIndex + direction + channels.length) % channels.length;
|
|
1216
|
+
const nextChannel = channels[nextIndex];
|
|
1217
|
+
if (!nextChannel) return false;
|
|
1218
|
+
changeChannel(nextChannel.id);
|
|
1219
|
+
return true;
|
|
1220
|
+
}, [changeChannel, channels, onChannelChange]);
|
|
1221
|
+
|
|
1222
|
+
const focusChannelSidebar = useCallback(() => {
|
|
1223
|
+
if (!showChannelSidebar || !onChannelChange) return false;
|
|
1224
|
+
if (inputFocused) {
|
|
1225
|
+
blurInput();
|
|
1226
|
+
}
|
|
1227
|
+
setSelectedIdx(-1);
|
|
1228
|
+
setSidebarFocused(true);
|
|
1229
|
+
return true;
|
|
1230
|
+
}, [blurInput, inputFocused, onChannelChange, setSidebarFocused, showChannelSidebar]);
|
|
1231
|
+
|
|
1232
|
+
const focusChatContent = useCallback(() => {
|
|
1233
|
+
if (!showChannelSidebar) return false;
|
|
1234
|
+
setSidebarFocused(false);
|
|
1235
|
+
return true;
|
|
1236
|
+
}, [setSidebarFocused, showChannelSidebar]);
|
|
1237
|
+
|
|
1238
|
+
const moveSidebarChannelSelection = useCallback((direction: "up" | "down") => {
|
|
1239
|
+
if (!showChannelSidebar || channels.length <= 1 || !onChannelChange) return false;
|
|
1240
|
+
const currentIndex = channels.findIndex((channel) => channel.id === channelIdRef.current);
|
|
1241
|
+
const baseIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
1242
|
+
const nextIndex = direction === "down"
|
|
1243
|
+
? Math.min(baseIndex + 1, channels.length - 1)
|
|
1244
|
+
: Math.max(baseIndex - 1, 0);
|
|
1245
|
+
const nextChannel = channels[nextIndex];
|
|
1246
|
+
if (!nextChannel || nextIndex === baseIndex) return true;
|
|
1247
|
+
changeChannel(nextChannel.id);
|
|
1248
|
+
return true;
|
|
1249
|
+
}, [changeChannel, channels, onChannelChange, showChannelSidebar]);
|
|
1250
|
+
|
|
1251
|
+
useEffect(() => {
|
|
1252
|
+
if (focused && showChannelSidebar) return;
|
|
1253
|
+
setSidebarFocused(false);
|
|
1254
|
+
}, [focused, setSidebarFocused, showChannelSidebar]);
|
|
1255
|
+
|
|
405
1256
|
useEffect(() => {
|
|
406
1257
|
if (!canSend && inputFocused) {
|
|
407
1258
|
blurInput();
|
|
@@ -416,16 +1267,20 @@ export function ChatContent({
|
|
|
416
1267
|
|
|
417
1268
|
useEffect(() => {
|
|
418
1269
|
if (focused && inputFocused) {
|
|
419
|
-
inputRef.current?.focus();
|
|
1270
|
+
inputRef.current?.focus?.();
|
|
420
1271
|
}
|
|
421
1272
|
}, [focused, inputFocused]);
|
|
422
1273
|
|
|
423
1274
|
const commitLocalDraft = useCallback((draft: string) => {
|
|
424
1275
|
if (applyingExternalDraftRef.current) return;
|
|
425
1276
|
inputValueRef.current = draft;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1277
|
+
updateComposerRows(draft);
|
|
1278
|
+
if (useDefaultControllerChannel) {
|
|
1279
|
+
controller.setDraft(draft);
|
|
1280
|
+
} else {
|
|
1281
|
+
controller.setChannelDraft(channelId, draft);
|
|
1282
|
+
}
|
|
1283
|
+
}, [channelId, controller, updateComposerRows, useDefaultControllerChannel]);
|
|
429
1284
|
|
|
430
1285
|
useEffect(() => {
|
|
431
1286
|
const textarea = inputRef.current;
|
|
@@ -451,6 +1306,39 @@ export function ChatContent({
|
|
|
451
1306
|
if (!focused) return;
|
|
452
1307
|
const isEnterKey = event.name === "return" || event.name === "enter";
|
|
453
1308
|
|
|
1309
|
+
if (sidebarFocusedRef.current && showChannelSidebar) {
|
|
1310
|
+
if (event.name === "left") {
|
|
1311
|
+
event.preventDefault?.();
|
|
1312
|
+
event.stopPropagation?.();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (event.name === "right") {
|
|
1317
|
+
event.preventDefault?.();
|
|
1318
|
+
event.stopPropagation?.();
|
|
1319
|
+
focusChatContent();
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (event.name === "up" || event.name === "down") {
|
|
1324
|
+
event.preventDefault?.();
|
|
1325
|
+
event.stopPropagation?.();
|
|
1326
|
+
moveSidebarChannelSelection(event.name);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (
|
|
1332
|
+
event.name === "left" &&
|
|
1333
|
+
showChannelSidebar &&
|
|
1334
|
+
(!inputFocused || inputValueRef.current.length === 0) &&
|
|
1335
|
+
focusChannelSidebar()
|
|
1336
|
+
) {
|
|
1337
|
+
event.preventDefault?.();
|
|
1338
|
+
event.stopPropagation?.();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
454
1342
|
if (inputFocused) {
|
|
455
1343
|
if (event.name === "escape") {
|
|
456
1344
|
event.preventDefault?.();
|
|
@@ -476,6 +1364,17 @@ export function ChatContent({
|
|
|
476
1364
|
return;
|
|
477
1365
|
}
|
|
478
1366
|
|
|
1367
|
+
if ((event.name === "]" || event.sequence === "]") && cycleChannel(1)) {
|
|
1368
|
+
event.preventDefault?.();
|
|
1369
|
+
event.stopPropagation?.();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if ((event.name === "[" || event.sequence === "[") && cycleChannel(-1)) {
|
|
1373
|
+
event.preventDefault?.();
|
|
1374
|
+
event.stopPropagation?.();
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
479
1378
|
if (canSend && isEnterKey && selectedIdx >= 0 && selectedIdx < messages.length) {
|
|
480
1379
|
event.preventDefault?.();
|
|
481
1380
|
event.stopPropagation?.();
|
|
@@ -542,28 +1441,10 @@ export function ChatContent({
|
|
|
542
1441
|
event.stopPropagation?.();
|
|
543
1442
|
setSelectedIdx(messages.length - 1);
|
|
544
1443
|
setFollowMessages(true);
|
|
545
|
-
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
1444
|
+
queueMicrotask(() => scrollToBottom(scrollRef.current, nativePaneChrome));
|
|
546
1445
|
return;
|
|
547
1446
|
}
|
|
548
|
-
}
|
|
549
|
-
beginReplyTo,
|
|
550
|
-
blurInput,
|
|
551
|
-
canSend,
|
|
552
|
-
clearReplyTarget,
|
|
553
|
-
focusComposer,
|
|
554
|
-
focused,
|
|
555
|
-
inputFocused,
|
|
556
|
-
hasOlderMessages,
|
|
557
|
-
loadingOlderMessages,
|
|
558
|
-
messages.length,
|
|
559
|
-
moveMessageSelection,
|
|
560
|
-
replyTo,
|
|
561
|
-
requestOlderMessages,
|
|
562
|
-
requestOlderMessagesIfNeeded,
|
|
563
|
-
returnToComposer,
|
|
564
|
-
selectedIdx,
|
|
565
|
-
shouldLeaveComposerForSelection,
|
|
566
|
-
]);
|
|
1447
|
+
});
|
|
567
1448
|
|
|
568
1449
|
const inputMetaHeight = canSend && replyTo ? 1 : 0;
|
|
569
1450
|
const composerBlockHeight = canSend
|
|
@@ -572,8 +1453,8 @@ export function ChatContent({
|
|
|
572
1453
|
const inputAreaHeight = canSend ? composerBlockHeight + inputMetaHeight : 2;
|
|
573
1454
|
const topSeparatorHeight = nativePaneChrome ? 0 : 1;
|
|
574
1455
|
const footerSeparatorHeight = !nativePaneChrome && !canSend ? 1 : 0;
|
|
575
|
-
const messageAreaHeight = Math.max(1, height - topSeparatorHeight - footerSeparatorHeight - inputAreaHeight);
|
|
576
|
-
const composerWidth = nativePaneChrome ?
|
|
1456
|
+
const messageAreaHeight = Math.max(1, height - compactChannelHeaderHeight - topSeparatorHeight - footerSeparatorHeight - inputAreaHeight);
|
|
1457
|
+
const composerWidth = nativePaneChrome ? chatWidth : contentWidth;
|
|
577
1458
|
|
|
578
1459
|
useEffect(() => {
|
|
579
1460
|
if (selectedIdx < messages.length) return;
|
|
@@ -582,8 +1463,8 @@ export function ChatContent({
|
|
|
582
1463
|
|
|
583
1464
|
useEffect(() => {
|
|
584
1465
|
if (!stickyTranscript) return;
|
|
585
|
-
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
586
|
-
}, [contentWidth, height, messages, messageAreaHeight, stickyTranscript]);
|
|
1466
|
+
queueMicrotask(() => scrollToBottom(scrollRef.current, nativePaneChrome));
|
|
1467
|
+
}, [contentWidth, height, messages, messageAreaHeight, nativePaneChrome, stickyTranscript]);
|
|
587
1468
|
|
|
588
1469
|
useEffect(() => {
|
|
589
1470
|
const anchor = prependAnchorRef.current;
|
|
@@ -593,14 +1474,19 @@ export function ChatContent({
|
|
|
593
1474
|
const currentAnchor = prependAnchorRef.current;
|
|
594
1475
|
const scrollBox = scrollRef.current;
|
|
595
1476
|
if (!currentAnchor || !scrollBox) return;
|
|
1477
|
+
if (pendingJumpMessageIdRef.current) {
|
|
1478
|
+
prependAnchorRef.current = null;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
596
1481
|
|
|
597
1482
|
const previousOldestIndex = currentAnchor.oldestMessageId
|
|
598
1483
|
? messages.findIndex((message) => message.id === currentAnchor.oldestMessageId)
|
|
599
1484
|
: -1;
|
|
600
|
-
const
|
|
1485
|
+
const exactPixels = nativePaneChrome === true && hasPixelScrollMetrics(scrollBox);
|
|
1486
|
+
const addedRows = !exactPixels && previousOldestIndex > 0
|
|
601
1487
|
? getMessageTopOffset(messages, previousOldestIndex, contentWidth)
|
|
602
|
-
: Math.max(0, scrollBox
|
|
603
|
-
scrollBox
|
|
1488
|
+
: Math.max(0, getScrollHeight(scrollBox, exactPixels) - currentAnchor.scrollHeight);
|
|
1489
|
+
scrollToPosition(scrollBox, currentAnchor.scrollTop + addedRows, exactPixels);
|
|
604
1490
|
if (currentAnchor.selectedMessageId) {
|
|
605
1491
|
const selectedMessageIndex = messages.findIndex((message) => message.id === currentAnchor.selectedMessageId);
|
|
606
1492
|
if (selectedMessageIndex >= 0) {
|
|
@@ -609,29 +1495,34 @@ export function ChatContent({
|
|
|
609
1495
|
}
|
|
610
1496
|
prependAnchorRef.current = null;
|
|
611
1497
|
});
|
|
612
|
-
}, [contentWidth, loadingOlderMessages, messages]);
|
|
1498
|
+
}, [contentWidth, loadingOlderMessages, messages, nativePaneChrome]);
|
|
613
1499
|
|
|
614
1500
|
useEffect(() => {
|
|
615
1501
|
if (!focused || !stickyTranscript) return;
|
|
616
|
-
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
617
|
-
}, [focused, stickyTranscript]);
|
|
1502
|
+
queueMicrotask(() => scrollToBottom(scrollRef.current, nativePaneChrome));
|
|
1503
|
+
}, [focused, nativePaneChrome, stickyTranscript]);
|
|
618
1504
|
|
|
619
1505
|
useEffect(() => {
|
|
620
1506
|
const sb = scrollRef.current;
|
|
621
1507
|
if (!sb) return;
|
|
622
1508
|
if (!selectionActive) return;
|
|
1509
|
+
if (nativePaneChrome) {
|
|
1510
|
+
const selectedMessageId = messages[selectedIdx]?.id ?? "";
|
|
1511
|
+
runAfterLayout(() => scrollElementIntoScrollBoxView(scrollRef.current, messageElementsRef.current.get(selectedMessageId)));
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
623
1514
|
const top = getMessageTopOffset(messages, selectedIdx, contentWidth);
|
|
624
1515
|
const rowHeight = estimateMessageHeight(messages[selectedIdx]!, contentWidth, isGroupedWithPrevious(messages, selectedIdx));
|
|
625
1516
|
const nextScrollTop = getSelectedMessageScrollTop({
|
|
626
1517
|
scrollTop: sb.scrollTop,
|
|
627
|
-
viewportHeight: sb.viewport
|
|
1518
|
+
viewportHeight: sb.viewport?.height ?? 0,
|
|
628
1519
|
top,
|
|
629
1520
|
rowHeight,
|
|
630
1521
|
});
|
|
631
1522
|
if (nextScrollTop !== sb.scrollTop) {
|
|
632
1523
|
sb.scrollTo(nextScrollTop);
|
|
633
1524
|
}
|
|
634
|
-
}, [contentWidth, messages, selectedIdx, selectionActive]);
|
|
1525
|
+
}, [contentWidth, messages, nativePaneChrome, selectedIdx, selectionActive]);
|
|
635
1526
|
|
|
636
1527
|
const replyPreview = replyTo
|
|
637
1528
|
? formatInlinePreview(
|
|
@@ -640,17 +1531,54 @@ export function ChatContent({
|
|
|
640
1531
|
)
|
|
641
1532
|
: "";
|
|
642
1533
|
const inputPlaceholder = replyTo ? `Reply to @${replyTo.user.username}...` : "Type a message...";
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
1534
|
+
const activeChannel = channels.find((channel) => channel.id === channelId);
|
|
1535
|
+
const activeChannelLabel = formatChannelLabel(activeChannel, channelId);
|
|
1536
|
+
const chatContentBg = focused && showChannelSidebar && !sidebarFocused
|
|
1537
|
+
? blendHex(colors.bg, colors.borderFocused, 0.08)
|
|
1538
|
+
: undefined;
|
|
1539
|
+
const chatLayoutHeight = nativePaneChrome ? "100%" : height;
|
|
1540
|
+
const nativeFillStyle = nativePaneChrome ? { minHeight: 0 } : undefined;
|
|
651
1541
|
|
|
652
1542
|
return (
|
|
653
|
-
<Box
|
|
1543
|
+
<Box
|
|
1544
|
+
flexDirection="row"
|
|
1545
|
+
width={width}
|
|
1546
|
+
height={chatLayoutHeight}
|
|
1547
|
+
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
1548
|
+
style={nativeFillStyle}
|
|
1549
|
+
>
|
|
1550
|
+
{showChannelSidebar && (
|
|
1551
|
+
<ChannelSidebar
|
|
1552
|
+
channels={channels}
|
|
1553
|
+
activeChannelId={channelId}
|
|
1554
|
+
width={channelSidebarWidth}
|
|
1555
|
+
height={height}
|
|
1556
|
+
focused={focused}
|
|
1557
|
+
keyboardFocused={sidebarFocused}
|
|
1558
|
+
loading={channelsLoading}
|
|
1559
|
+
onSelect={changeChannel}
|
|
1560
|
+
onFocusRequest={() => setSidebarFocused(true)}
|
|
1561
|
+
/>
|
|
1562
|
+
)}
|
|
1563
|
+
|
|
1564
|
+
<Box
|
|
1565
|
+
flexDirection="column"
|
|
1566
|
+
width={chatWidth}
|
|
1567
|
+
height={chatLayoutHeight}
|
|
1568
|
+
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
1569
|
+
backgroundColor={chatContentBg}
|
|
1570
|
+
onMouseDown={() => setSidebarFocused(false)}
|
|
1571
|
+
style={nativeFillStyle}
|
|
1572
|
+
>
|
|
1573
|
+
{compactChannelHeaderHeight > 0 && (
|
|
1574
|
+
<Box height={1} width={contentWidth} flexDirection="row">
|
|
1575
|
+
<Text fg={colors.textDim}>#</Text>
|
|
1576
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>
|
|
1577
|
+
{truncateChannelLabel(activeChannelLabel, Math.max(contentWidth - 1, 1))}
|
|
1578
|
+
</Text>
|
|
1579
|
+
</Box>
|
|
1580
|
+
)}
|
|
1581
|
+
|
|
654
1582
|
{!nativePaneChrome && (
|
|
655
1583
|
<Box height={1} width={contentWidth}>
|
|
656
1584
|
<Text fg={colors.border}>{"-".repeat(contentWidth)}</Text>
|
|
@@ -659,155 +1587,64 @@ export function ChatContent({
|
|
|
659
1587
|
|
|
660
1588
|
<ScrollBox
|
|
661
1589
|
ref={scrollRef}
|
|
662
|
-
height={messageAreaHeight}
|
|
1590
|
+
height={nativePaneChrome ? undefined : messageAreaHeight}
|
|
1591
|
+
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
663
1592
|
scrollY
|
|
664
1593
|
focusable={false}
|
|
665
1594
|
stickyScroll={stickyTranscript}
|
|
666
1595
|
stickyStart="bottom"
|
|
667
1596
|
onMouseScroll={() => queueMicrotask(requestOlderMessagesIfNeeded)}
|
|
668
|
-
style={
|
|
1597
|
+
style={nativePaneChrome ? { minHeight: 0 } : undefined}
|
|
669
1598
|
>
|
|
670
1599
|
{loadingOlderMessages && (
|
|
671
1600
|
<Box alignItems="center" justifyContent="center" height={1} width={contentWidth}>
|
|
672
1601
|
<Text fg={colors.textDim}>Loading earlier messages...</Text>
|
|
673
1602
|
</Box>
|
|
674
1603
|
)}
|
|
675
|
-
{messages.length === 0
|
|
1604
|
+
{loading && messages.length === 0 ? (
|
|
1605
|
+
<Box alignItems="center" justifyContent="center" flexGrow={1}>
|
|
1606
|
+
<Text fg={colors.textDim}>Loading...</Text>
|
|
1607
|
+
</Box>
|
|
1608
|
+
) : messages.length === 0 && (
|
|
676
1609
|
<Box alignItems="center" justifyContent="center" flexGrow={1}>
|
|
677
1610
|
<Text fg={colors.textDim}>No messages yet. Be the first to say something!</Text>
|
|
678
1611
|
</Box>
|
|
679
1612
|
)}
|
|
680
|
-
{messages.map((msg, index) =>
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const showReplyAction = canSend && (isSelected || hoveredIdx === index);
|
|
684
|
-
const bgColor = isSelected ? colors.selected : isHovered ? hoverBg() : undefined;
|
|
685
|
-
const grouped = isGroupedWithPrevious(messages, index);
|
|
686
|
-
const isSending = msg.clientStatus === "sending";
|
|
687
|
-
const hasFailed = msg.clientStatus === "failed";
|
|
688
|
-
const selectedTextColor = hasFailed ? colors.negative : colors.selectedText;
|
|
689
|
-
const replyMetaColor = isSelected ? selectedTextColor : colors.textMuted;
|
|
690
|
-
const replyAuthorColor = isSelected ? selectedTextColor : colors.textDim;
|
|
691
|
-
const headerStatus = isSending ? "sending..." : hasFailed ? "failed" : formatTimeAgo(msg.createdAt);
|
|
692
|
-
const headerStatusColor = isSelected
|
|
693
|
-
? selectedTextColor
|
|
694
|
-
: isSending
|
|
695
|
-
? colors.textDim
|
|
696
|
-
: hasFailed
|
|
697
|
-
? colors.negative
|
|
698
|
-
: colors.textMuted;
|
|
699
|
-
const authorColor = isSelected ? selectedTextColor : hasFailed ? colors.negative : colors.positive;
|
|
700
|
-
const authorAttributes = (isSending ? TextAttributes.DIM : 0) | TextAttributes.BOLD;
|
|
701
|
-
const bodyColor = isSelected ? selectedTextColor : hasFailed ? colors.negative : isSending ? colors.textDim : colors.text;
|
|
702
|
-
const bodyLines = getMessageBodyLines(msg, contentWidth);
|
|
703
|
-
const showInlineReplyAction = !grouped && (nativePaneChrome ? canSend : showReplyAction);
|
|
704
|
-
const showGroupedReplyAction = grouped && (nativePaneChrome ? canSend : showReplyAction);
|
|
705
|
-
const setHovered = () => setHoveredIdx((current) => (current === index ? current : index));
|
|
706
|
-
const clearHovered = () => setHoveredIdx((current) => (current === index ? null : current));
|
|
707
|
-
const messageRowProps = {
|
|
708
|
-
width: contentWidth,
|
|
709
|
-
backgroundColor: bgColor,
|
|
710
|
-
"data-gloom-role": nativePaneChrome ? "chat-message-row" : undefined,
|
|
711
|
-
"data-selected": nativePaneChrome ? (isSelected ? "true" : "false") : undefined,
|
|
712
|
-
onMouseMove: nativePaneChrome ? undefined : setHovered,
|
|
713
|
-
onMouseOut: nativePaneChrome ? undefined : clearHovered,
|
|
714
|
-
};
|
|
715
|
-
return (
|
|
716
|
-
<Box
|
|
1613
|
+
{messages.map((msg, index) => (
|
|
1614
|
+
nativePaneChrome ? (
|
|
1615
|
+
<DesktopChatMessage
|
|
717
1616
|
key={msg.id}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
{
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
{
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
{msg.user.username ?? "anon"}
|
|
750
|
-
</Text>
|
|
751
|
-
<Text fg={headerStatusColor}> {headerStatus}</Text>
|
|
752
|
-
{showInlineReplyAction && (
|
|
753
|
-
<>
|
|
754
|
-
<Text fg={headerStatusColor}> </Text>
|
|
755
|
-
<Box
|
|
756
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
757
|
-
height={1}
|
|
758
|
-
data-gloom-role={nativePaneChrome ? "chat-message-reply-action" : undefined}
|
|
759
|
-
>
|
|
760
|
-
<ChatActionChip
|
|
761
|
-
label="Reply"
|
|
762
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
763
|
-
emphasized={isSelected}
|
|
764
|
-
onPress={() => beginReplyTo(index)}
|
|
765
|
-
/>
|
|
766
|
-
</Box>
|
|
767
|
-
</>
|
|
768
|
-
)}
|
|
769
|
-
</Box>
|
|
770
|
-
)}
|
|
771
|
-
{bodyLines.map((line, lineIndex) => (
|
|
772
|
-
<Box
|
|
773
|
-
key={`${msg.id}:body:${lineIndex}`}
|
|
774
|
-
{...messageRowProps}
|
|
775
|
-
paddingLeft={3}
|
|
776
|
-
height={1}
|
|
777
|
-
flexDirection="row"
|
|
778
|
-
position={grouped ? "relative" : undefined}
|
|
779
|
-
>
|
|
780
|
-
<Box width={messageBodyWidth} height={1}>
|
|
781
|
-
<TickerBadgeText
|
|
782
|
-
text={line}
|
|
783
|
-
lineWidth={messageBodyWidth}
|
|
784
|
-
catalog={catalog}
|
|
785
|
-
textColor={bodyColor}
|
|
786
|
-
openTicker={openTicker}
|
|
787
|
-
/>
|
|
788
|
-
</Box>
|
|
789
|
-
{lineIndex === 0 && showGroupedReplyAction && (
|
|
790
|
-
<Box
|
|
791
|
-
position="absolute"
|
|
792
|
-
top={0}
|
|
793
|
-
right={0}
|
|
794
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
795
|
-
height={1}
|
|
796
|
-
data-gloom-role={nativePaneChrome ? "chat-message-reply-action" : undefined}
|
|
797
|
-
>
|
|
798
|
-
<ChatActionChip
|
|
799
|
-
label="Reply"
|
|
800
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
801
|
-
emphasized={isSelected}
|
|
802
|
-
onPress={() => beginReplyTo(index)}
|
|
803
|
-
/>
|
|
804
|
-
</Box>
|
|
805
|
-
)}
|
|
806
|
-
</Box>
|
|
807
|
-
))}
|
|
808
|
-
</Box>
|
|
809
|
-
);
|
|
810
|
-
})}
|
|
1617
|
+
msg={msg}
|
|
1618
|
+
index={index}
|
|
1619
|
+
messages={messages}
|
|
1620
|
+
selectedIdx={selectedIdx}
|
|
1621
|
+
hoveredIdx={hoveredIdx}
|
|
1622
|
+
canSend={canSend}
|
|
1623
|
+
catalog={catalog}
|
|
1624
|
+
openTicker={openTicker}
|
|
1625
|
+
beginReplyTo={beginReplyTo}
|
|
1626
|
+
jumpToMessage={jumpToMessage}
|
|
1627
|
+
registerMessageElement={registerMessageElement}
|
|
1628
|
+
/>
|
|
1629
|
+
) : (
|
|
1630
|
+
<TerminalChatMessage
|
|
1631
|
+
key={msg.id}
|
|
1632
|
+
msg={msg}
|
|
1633
|
+
index={index}
|
|
1634
|
+
messages={messages}
|
|
1635
|
+
selectedIdx={selectedIdx}
|
|
1636
|
+
hoveredIdx={hoveredIdx}
|
|
1637
|
+
canSend={canSend}
|
|
1638
|
+
contentWidth={contentWidth}
|
|
1639
|
+
messageBodyWidth={messageBodyWidth}
|
|
1640
|
+
catalog={catalog}
|
|
1641
|
+
openTicker={openTicker}
|
|
1642
|
+
beginReplyTo={beginReplyTo}
|
|
1643
|
+
jumpToMessage={jumpToMessage}
|
|
1644
|
+
setHoveredIdx={setHoveredIdx}
|
|
1645
|
+
/>
|
|
1646
|
+
)
|
|
1647
|
+
))}
|
|
811
1648
|
</ScrollBox>
|
|
812
1649
|
|
|
813
1650
|
{!nativePaneChrome && !canSend && (
|
|
@@ -834,7 +1671,7 @@ export function ChatContent({
|
|
|
834
1671
|
|
|
835
1672
|
<MessageComposer
|
|
836
1673
|
inputRef={inputRef}
|
|
837
|
-
initialValue={
|
|
1674
|
+
initialValue={inputValueRef.current}
|
|
838
1675
|
focused={inputFocused && focused}
|
|
839
1676
|
placeholder={inputPlaceholder}
|
|
840
1677
|
terminalPrefix=" > "
|
|
@@ -878,23 +1715,85 @@ export function ChatContent({
|
|
|
878
1715
|
)}
|
|
879
1716
|
</Box>
|
|
880
1717
|
)}
|
|
1718
|
+
</Box>
|
|
881
1719
|
</Box>
|
|
882
1720
|
);
|
|
883
1721
|
}
|
|
884
1722
|
|
|
885
|
-
|
|
1723
|
+
function ChatPane({ focused, width, height, close }: PaneProps) {
|
|
1724
|
+
const dispatch = useAppDispatch();
|
|
1725
|
+
const stateRef = useAppStateRef();
|
|
1726
|
+
const paneId = usePaneInstanceId();
|
|
1727
|
+
const pane = usePaneInstance();
|
|
1728
|
+
const rawPaneChannelId = typeof pane?.settings?.channelId === "string" ? pane.settings.channelId : null;
|
|
1729
|
+
const lastVisitedChannelId = useAppSelector((state) => (
|
|
1730
|
+
(state.config.pluginConfig["gloomberb-cloud"]?.[LAST_VISITED_CHAT_CHANNEL_KEY] as string | undefined) ??
|
|
1731
|
+
DEFAULT_CHAT_CHANNEL_ID
|
|
1732
|
+
));
|
|
1733
|
+
const initialChannelIdRef = useRef(normalizeChannelId(rawPaneChannelId ?? lastVisitedChannelId));
|
|
1734
|
+
const persistedChannelId = normalizeChannelId(rawPaneChannelId ?? initialChannelIdRef.current);
|
|
1735
|
+
const persistChannelId = useCallback((nextChannelId: string) => {
|
|
1736
|
+
const currentState = stateRef.current;
|
|
1737
|
+
const layout = setPaneSetting(currentState.config.layout, paneId, "channelId", nextChannelId);
|
|
1738
|
+
const layouts = currentState.config.layouts.map((savedLayout, index) => (
|
|
1739
|
+
index === currentState.config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
|
|
1740
|
+
));
|
|
1741
|
+
const pluginConfig = {
|
|
1742
|
+
...currentState.config.pluginConfig,
|
|
1743
|
+
"gloomberb-cloud": {
|
|
1744
|
+
...(currentState.config.pluginConfig["gloomberb-cloud"] ?? {}),
|
|
1745
|
+
[LAST_VISITED_CHAT_CHANNEL_KEY]: nextChannelId,
|
|
1746
|
+
},
|
|
1747
|
+
};
|
|
1748
|
+
const nextConfig = {
|
|
1749
|
+
...currentState.config,
|
|
1750
|
+
layout,
|
|
1751
|
+
layouts,
|
|
1752
|
+
pluginConfig,
|
|
1753
|
+
};
|
|
1754
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
1755
|
+
scheduleConfigSave(nextConfig);
|
|
1756
|
+
}, [dispatch, paneId, stateRef]);
|
|
1757
|
+
const [channelId, setLocalChannelId] = useState(initialChannelIdRef.current);
|
|
1758
|
+
const pendingChannelIdRef = useRef<string | null>(null);
|
|
1759
|
+
|
|
1760
|
+
useEffect(() => {
|
|
1761
|
+
if (rawPaneChannelId) return;
|
|
1762
|
+
persistChannelId(initialChannelIdRef.current);
|
|
1763
|
+
}, [persistChannelId, rawPaneChannelId]);
|
|
1764
|
+
|
|
1765
|
+
useEffect(() => {
|
|
1766
|
+
const normalizedPersisted = normalizeChannelId(persistedChannelId);
|
|
1767
|
+
if (pendingChannelIdRef.current) {
|
|
1768
|
+
if (pendingChannelIdRef.current === normalizedPersisted) {
|
|
1769
|
+
pendingChannelIdRef.current = null;
|
|
1770
|
+
}
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
setLocalChannelId((current) => (current === normalizedPersisted ? current : normalizedPersisted));
|
|
1774
|
+
}, [persistedChannelId]);
|
|
1775
|
+
|
|
1776
|
+
const setChannelId = useCallback((nextChannelId: string) => {
|
|
1777
|
+
const normalized = normalizeChannelId(nextChannelId);
|
|
1778
|
+
pendingChannelIdRef.current = normalized;
|
|
1779
|
+
setLocalChannelId((current) => (current === normalized ? current : normalized));
|
|
1780
|
+
persistChannelId(normalized);
|
|
1781
|
+
}, [persistChannelId]);
|
|
1782
|
+
|
|
886
1783
|
return (
|
|
887
1784
|
<ChatContent
|
|
888
1785
|
width={width}
|
|
889
1786
|
height={height}
|
|
890
1787
|
focused={focused}
|
|
891
1788
|
close={close}
|
|
1789
|
+
channelId={channelId}
|
|
1790
|
+
onChannelChange={setChannelId}
|
|
892
1791
|
/>
|
|
893
1792
|
);
|
|
894
1793
|
}
|
|
895
1794
|
|
|
896
1795
|
export function ChatStatusWidget({ controller = chatController }: ChatStatusWidgetProps) {
|
|
897
|
-
const {
|
|
1796
|
+
const { showPane } = usePluginAppActions();
|
|
898
1797
|
const cloudPluginDisabled = useAppSelector((state) => state.config.disabledPlugins.includes("gloomberb-cloud"));
|
|
899
1798
|
const initialSnapshot = controller.getSnapshot();
|
|
900
1799
|
const [username, setUsername] = useState<string | null>(initialSnapshot.user?.username ?? null);
|
|
@@ -905,7 +1804,7 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
905
1804
|
const openChat = (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
906
1805
|
event?.preventDefault?.();
|
|
907
1806
|
event?.stopPropagation?.();
|
|
908
|
-
|
|
1807
|
+
showPane("chat");
|
|
909
1808
|
};
|
|
910
1809
|
|
|
911
1810
|
useEffect(() => {
|
|
@@ -968,8 +1867,16 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
968
1867
|
label: "New Chat Pane",
|
|
969
1868
|
description: "Open another floating chat window",
|
|
970
1869
|
keywords: ["new", "chat", "pane", "message"],
|
|
971
|
-
shortcut: { prefix: "CHAT" },
|
|
972
|
-
createInstance: () =>
|
|
1870
|
+
shortcut: { prefix: "CHAT", argPlaceholder: "channel", argKind: "text" },
|
|
1871
|
+
createInstance: async (context, options) => {
|
|
1872
|
+
const channelId = options?.arg
|
|
1873
|
+
? await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(options.arg))
|
|
1874
|
+
: await chatController.resolvePreferredChannelId(getLastVisitedChatChannelId(context.config));
|
|
1875
|
+
return {
|
|
1876
|
+
placement: "floating",
|
|
1877
|
+
settings: { channelId },
|
|
1878
|
+
};
|
|
1879
|
+
},
|
|
973
1880
|
},
|
|
974
1881
|
],
|
|
975
1882
|
|
|
@@ -1056,9 +1963,9 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
1056
1963
|
execute: () => {
|
|
1057
1964
|
const registry = getSharedRegistry();
|
|
1058
1965
|
if (registry?.isPaneFloating("chat")) {
|
|
1059
|
-
ctx.
|
|
1966
|
+
ctx.hidePane("chat");
|
|
1060
1967
|
} else {
|
|
1061
|
-
ctx.
|
|
1968
|
+
ctx.showPane("chat");
|
|
1062
1969
|
}
|
|
1063
1970
|
},
|
|
1064
1971
|
});
|
|
@@ -1086,7 +1993,7 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
1086
1993
|
}
|
|
1087
1994
|
chatController.clearSession();
|
|
1088
1995
|
await chatController.refreshSession();
|
|
1089
|
-
ctx.
|
|
1996
|
+
ctx.showPane("chat");
|
|
1090
1997
|
},
|
|
1091
1998
|
});
|
|
1092
1999
|
|
|
@@ -1122,7 +2029,7 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
1122
2029
|
await apiClient.sendVerification();
|
|
1123
2030
|
chatController.clearSession();
|
|
1124
2031
|
await chatController.refreshSession();
|
|
1125
|
-
ctx.
|
|
2032
|
+
ctx.showPane("chat");
|
|
1126
2033
|
},
|
|
1127
2034
|
});
|
|
1128
2035
|
|