gloomberb 0.7.0 → 0.7.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/package.json +1 -1
- package/src/app.tsx +149 -51
- package/src/capabilities/factories.ts +3 -0
- package/src/components/chart/chart-renderer.ts +24 -12
- package/src/components/chart/chart.test.ts +43 -0
- 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 +15 -12
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +2 -0
- package/src/components/chart/stock-chart.tsx +31 -20
- package/src/components/chart/time-axis-label.tsx +78 -1
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +114 -31
- package/src/components/command-bar/command-bar.tsx +587 -335
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +1 -2
- 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 -1
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +5 -2
- package/src/components/layout/header.tsx +2 -0
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- 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 +198 -0
- package/src/components/layout/shell.tsx +33 -2
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/speedometer-gauge.tsx +29 -25
- package/src/components/table-view-shared.tsx +85 -3
- package/src/components/ticker-list-table-view.tsx +16 -2
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/data-table.tsx +12 -9
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/table-layout.test.ts +27 -1
- package/src/components/ui/table-layout.ts +39 -1
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +68 -1
- package/src/core/state/app-state.ts +4 -14
- package/src/market-data/coordinator.test.ts +75 -0
- package/src/market-data/coordinator.ts +218 -48
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -0
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -2
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/chat-controller.test.ts +83 -1
- package/src/plugins/builtin/chat-controller.ts +491 -243
- package/src/plugins/builtin/chat.test.tsx +425 -7
- package/src/plugins/builtin/chat.tsx +1109 -211
- package/src/plugins/builtin/cloud-tweets.tsx +1 -0
- package/src/plugins/builtin/fear-greed/index.tsx +148 -39
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +54 -14
- package/src/plugins/builtin/market-movers/index.tsx +12 -18
- 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/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/notes.tsx +1 -0
- package/src/plugins/builtin/options.tsx +2 -16
- 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 +159 -12
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/plugin-runtime.test.tsx +34 -0
- package/src/plugins/plugin-runtime.tsx +7 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/registry.ts +3 -3
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +1 -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 +111 -96
- package/src/renderers/electrobun/view/desktop-controls.tsx +8 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -0
- package/src/renderers/electrobun/view/input-host.tsx +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +6 -8
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +101 -5
- package/src/renderers/opentui/input-host.tsx +1 -1
- 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 -11
- 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 +20 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.ts +13 -10
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +64 -3
- package/src/theme/theme-context.tsx +26 -0
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -1
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -1
- package/src/ui/host.tsx +14 -2
- package/src/ui/index.tsx +1 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- 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
|
@@ -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,17 +348,552 @@ 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);
|
|
225
887
|
const { nativePaneChrome } = useUiCapabilities();
|
|
226
|
-
const
|
|
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);
|
|
227
897
|
const composerPrefixWidth = nativePaneChrome ? 0 : 3;
|
|
228
898
|
const composerTextWidth = Math.max(contentWidth - composerPrefixWidth, 1);
|
|
229
899
|
const inputValueRef = useRef(initialSnapshot.draft);
|
|
@@ -237,12 +907,19 @@ export function ChatContent({
|
|
|
237
907
|
const [composerRows, setComposerRows] = useState(() => estimateComposerHeight(initialSnapshot.draft, composerTextWidth));
|
|
238
908
|
const [selectedIdx, setSelectedIdx] = useState(-1);
|
|
239
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
|
+
}, []);
|
|
240
916
|
const [followMessages, setFollowMessages] = useState(true);
|
|
241
917
|
const [replyTo, setReplyTo] = useState<ChatMessage | null>(() => (
|
|
242
918
|
initialSnapshot.replyToId ? initialSnapshot.messages.find((message) => message.id === initialSnapshot.replyToId) ?? null : null
|
|
243
919
|
));
|
|
244
920
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
245
921
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
922
|
+
const messageElementsRef = useRef(new Map<string, unknown>());
|
|
246
923
|
const applyingExternalDraftRef = useRef(false);
|
|
247
924
|
const prependAnchorRef = useRef<{
|
|
248
925
|
oldestMessageId: string | null;
|
|
@@ -251,7 +928,7 @@ export function ChatContent({
|
|
|
251
928
|
selectedMessageId: string | null;
|
|
252
929
|
} | null>(null);
|
|
253
930
|
const canSend = !!user?.emailVerified;
|
|
254
|
-
const
|
|
931
|
+
const useDefaultControllerChannel = channelId === DEFAULT_CHAT_CHANNEL_ID && !onChannelChange;
|
|
255
932
|
const messageBodyWidth = Math.max(contentWidth - 4, 1);
|
|
256
933
|
const composerHeight = canSend
|
|
257
934
|
? nativePaneChrome
|
|
@@ -266,16 +943,51 @@ export function ChatContent({
|
|
|
266
943
|
}, [composerTextWidth]);
|
|
267
944
|
|
|
268
945
|
useEffect(() => {
|
|
269
|
-
return controller.attachView();
|
|
270
|
-
}, [controller]);
|
|
946
|
+
return useDefaultControllerChannel ? controller.attachView() : controller.attachChannelView(channelId);
|
|
947
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
271
948
|
|
|
272
949
|
useEffect(() => {
|
|
273
950
|
updateComposerRows(inputValueRef.current);
|
|
274
951
|
}, [updateComposerRows]);
|
|
275
952
|
|
|
276
953
|
useEffect(() => {
|
|
277
|
-
|
|
954
|
+
void controller.refreshChannels().catch(() => {});
|
|
955
|
+
void controller.refreshSession().catch(() => {});
|
|
956
|
+
}, [controller]);
|
|
957
|
+
|
|
958
|
+
useEffect(() => {
|
|
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"]>) {
|
|
278
988
|
setMessages(snapshot.messages);
|
|
989
|
+
setChannels(snapshot.channels);
|
|
990
|
+
setChannelsLoading(snapshot.channelsLoading);
|
|
279
991
|
setHasSavedSession(snapshot.hasSavedSession);
|
|
280
992
|
setUser(snapshot.user);
|
|
281
993
|
setLoading(snapshot.loading);
|
|
@@ -294,23 +1006,33 @@ export function ChatContent({
|
|
|
294
1006
|
setReplyTo(snapshot.replyToId
|
|
295
1007
|
? snapshot.messages.find((message) => message.id === snapshot.replyToId) ?? null
|
|
296
1008
|
: null);
|
|
297
|
-
}
|
|
1009
|
+
}
|
|
298
1010
|
|
|
299
|
-
void controller.
|
|
300
|
-
void controller.refreshMessages().catch(() => {});
|
|
1011
|
+
void (useDefaultControllerChannel ? controller.refreshMessages() : controller.refreshChannelMessages(channelId)).catch(() => {});
|
|
301
1012
|
return unsubscribe;
|
|
302
|
-
}, [controller, updateComposerRows]);
|
|
1013
|
+
}, [channelId, controller, updateComposerRows, useDefaultControllerChannel]);
|
|
303
1014
|
|
|
304
|
-
const
|
|
1015
|
+
const messageContents = useMemo(() => messages.map((message) => message.content), [messages]);
|
|
1016
|
+
const { catalog, openTicker } = useInlineTickers(messageContents);
|
|
305
1017
|
|
|
306
1018
|
const replyToRef = useRef(replyTo);
|
|
307
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
|
+
}, []);
|
|
308
1029
|
|
|
309
1030
|
const focusInput = useCallback(() => {
|
|
1031
|
+
setSidebarFocused(false);
|
|
310
1032
|
setInputFocused(true);
|
|
311
1033
|
dispatch({ type: "SET_INPUT_CAPTURED", captured: true });
|
|
312
|
-
inputRef.current?.focus();
|
|
313
|
-
}, [dispatch]);
|
|
1034
|
+
inputRef.current?.focus?.();
|
|
1035
|
+
}, [dispatch, setSidebarFocused]);
|
|
314
1036
|
|
|
315
1037
|
const focusComposer = useCallback(() => {
|
|
316
1038
|
setSelectedIdx(-1);
|
|
@@ -325,8 +1047,12 @@ export function ChatContent({
|
|
|
325
1047
|
|
|
326
1048
|
const clearReplyTarget = useCallback(() => {
|
|
327
1049
|
setReplyTo(null);
|
|
328
|
-
|
|
329
|
-
|
|
1050
|
+
if (useDefaultControllerChannel) {
|
|
1051
|
+
controller.setReplyToId(null);
|
|
1052
|
+
} else {
|
|
1053
|
+
controller.setChannelReplyToId(channelId, null);
|
|
1054
|
+
}
|
|
1055
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
330
1056
|
|
|
331
1057
|
const beginReplyTo = useCallback((index: number, options?: { deferFocus?: boolean }) => {
|
|
332
1058
|
if (!canSend || index < 0 || index >= messages.length) return;
|
|
@@ -335,13 +1061,17 @@ export function ChatContent({
|
|
|
335
1061
|
setSelectedIdx(index);
|
|
336
1062
|
setFollowMessages(index === messages.length - 1);
|
|
337
1063
|
setReplyTo(nextReplyTo);
|
|
338
|
-
|
|
1064
|
+
if (useDefaultControllerChannel) {
|
|
1065
|
+
controller.setReplyToId(nextReplyTo.id);
|
|
1066
|
+
} else {
|
|
1067
|
+
controller.setChannelReplyToId(channelId, nextReplyTo.id);
|
|
1068
|
+
}
|
|
339
1069
|
if (options?.deferFocus) {
|
|
340
1070
|
queueMicrotask(() => focusInput());
|
|
341
1071
|
} else {
|
|
342
1072
|
focusInput();
|
|
343
1073
|
}
|
|
344
|
-
}, [canSend, controller, focusInput, messages]);
|
|
1074
|
+
}, [canSend, channelId, controller, focusInput, messages, useDefaultControllerChannel]);
|
|
345
1075
|
|
|
346
1076
|
const returnToComposer = useCallback(() => {
|
|
347
1077
|
setSelectedIdx(-1);
|
|
@@ -384,26 +1114,34 @@ export function ChatContent({
|
|
|
384
1114
|
const sendMessage = useCallback(() => {
|
|
385
1115
|
const content = inputValueRef.current.trim();
|
|
386
1116
|
if (!content) return;
|
|
387
|
-
|
|
1117
|
+
if (useDefaultControllerChannel) {
|
|
1118
|
+
controller.send(content, replyToRef.current?.id);
|
|
1119
|
+
} else {
|
|
1120
|
+
controller.sendToChannel(channelId, content, replyToRef.current?.id);
|
|
1121
|
+
}
|
|
388
1122
|
setSelectedIdx(-1);
|
|
389
1123
|
setFollowMessages(true);
|
|
390
|
-
}, [controller]);
|
|
1124
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
391
1125
|
|
|
392
1126
|
const requestOlderMessages = useCallback(() => {
|
|
393
1127
|
const scrollBox = scrollRef.current;
|
|
394
1128
|
if (!scrollBox || loadingOlderMessages || !hasOlderMessages || messages.length === 0) return;
|
|
1129
|
+
const exactPixels = nativePaneChrome === true && hasPixelScrollMetrics(scrollBox);
|
|
395
1130
|
|
|
396
1131
|
prependAnchorRef.current = {
|
|
397
1132
|
oldestMessageId: messages[0]?.id ?? null,
|
|
398
|
-
scrollHeight: scrollBox
|
|
399
|
-
scrollTop: scrollBox
|
|
1133
|
+
scrollHeight: getScrollHeight(scrollBox, exactPixels),
|
|
1134
|
+
scrollTop: getScrollTop(scrollBox, exactPixels),
|
|
400
1135
|
selectedMessageId: selectedIdx >= 0 ? messages[selectedIdx]?.id ?? null : null,
|
|
401
1136
|
};
|
|
402
1137
|
setFollowMessages(false);
|
|
403
|
-
|
|
1138
|
+
const request = useDefaultControllerChannel
|
|
1139
|
+
? controller.loadOlderMessages()
|
|
1140
|
+
: controller.loadOlderChannelMessages(channelId);
|
|
1141
|
+
void request.catch(() => {
|
|
404
1142
|
prependAnchorRef.current = null;
|
|
405
1143
|
});
|
|
406
|
-
}, [controller, hasOlderMessages, loadingOlderMessages, messages, selectedIdx]);
|
|
1144
|
+
}, [channelId, controller, hasOlderMessages, loadingOlderMessages, messages, nativePaneChrome, selectedIdx, useDefaultControllerChannel]);
|
|
407
1145
|
|
|
408
1146
|
const requestOlderMessagesIfNeeded = useCallback(() => {
|
|
409
1147
|
const scrollBox = scrollRef.current;
|
|
@@ -411,6 +1149,110 @@ export function ChatContent({
|
|
|
411
1149
|
requestOlderMessages();
|
|
412
1150
|
}, [requestOlderMessages]);
|
|
413
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
|
+
|
|
414
1256
|
useEffect(() => {
|
|
415
1257
|
if (!canSend && inputFocused) {
|
|
416
1258
|
blurInput();
|
|
@@ -425,7 +1267,7 @@ export function ChatContent({
|
|
|
425
1267
|
|
|
426
1268
|
useEffect(() => {
|
|
427
1269
|
if (focused && inputFocused) {
|
|
428
|
-
inputRef.current?.focus();
|
|
1270
|
+
inputRef.current?.focus?.();
|
|
429
1271
|
}
|
|
430
1272
|
}, [focused, inputFocused]);
|
|
431
1273
|
|
|
@@ -433,8 +1275,12 @@ export function ChatContent({
|
|
|
433
1275
|
if (applyingExternalDraftRef.current) return;
|
|
434
1276
|
inputValueRef.current = draft;
|
|
435
1277
|
updateComposerRows(draft);
|
|
436
|
-
|
|
437
|
-
|
|
1278
|
+
if (useDefaultControllerChannel) {
|
|
1279
|
+
controller.setDraft(draft);
|
|
1280
|
+
} else {
|
|
1281
|
+
controller.setChannelDraft(channelId, draft);
|
|
1282
|
+
}
|
|
1283
|
+
}, [channelId, controller, updateComposerRows, useDefaultControllerChannel]);
|
|
438
1284
|
|
|
439
1285
|
useEffect(() => {
|
|
440
1286
|
const textarea = inputRef.current;
|
|
@@ -460,6 +1306,39 @@ export function ChatContent({
|
|
|
460
1306
|
if (!focused) return;
|
|
461
1307
|
const isEnterKey = event.name === "return" || event.name === "enter";
|
|
462
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
|
+
|
|
463
1342
|
if (inputFocused) {
|
|
464
1343
|
if (event.name === "escape") {
|
|
465
1344
|
event.preventDefault?.();
|
|
@@ -485,6 +1364,17 @@ export function ChatContent({
|
|
|
485
1364
|
return;
|
|
486
1365
|
}
|
|
487
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
|
+
|
|
488
1378
|
if (canSend && isEnterKey && selectedIdx >= 0 && selectedIdx < messages.length) {
|
|
489
1379
|
event.preventDefault?.();
|
|
490
1380
|
event.stopPropagation?.();
|
|
@@ -551,28 +1441,10 @@ export function ChatContent({
|
|
|
551
1441
|
event.stopPropagation?.();
|
|
552
1442
|
setSelectedIdx(messages.length - 1);
|
|
553
1443
|
setFollowMessages(true);
|
|
554
|
-
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
1444
|
+
queueMicrotask(() => scrollToBottom(scrollRef.current, nativePaneChrome));
|
|
555
1445
|
return;
|
|
556
1446
|
}
|
|
557
|
-
}
|
|
558
|
-
beginReplyTo,
|
|
559
|
-
blurInput,
|
|
560
|
-
canSend,
|
|
561
|
-
clearReplyTarget,
|
|
562
|
-
focusComposer,
|
|
563
|
-
focused,
|
|
564
|
-
inputFocused,
|
|
565
|
-
hasOlderMessages,
|
|
566
|
-
loadingOlderMessages,
|
|
567
|
-
messages.length,
|
|
568
|
-
moveMessageSelection,
|
|
569
|
-
replyTo,
|
|
570
|
-
requestOlderMessages,
|
|
571
|
-
requestOlderMessagesIfNeeded,
|
|
572
|
-
returnToComposer,
|
|
573
|
-
selectedIdx,
|
|
574
|
-
shouldLeaveComposerForSelection,
|
|
575
|
-
]);
|
|
1447
|
+
});
|
|
576
1448
|
|
|
577
1449
|
const inputMetaHeight = canSend && replyTo ? 1 : 0;
|
|
578
1450
|
const composerBlockHeight = canSend
|
|
@@ -581,8 +1453,8 @@ export function ChatContent({
|
|
|
581
1453
|
const inputAreaHeight = canSend ? composerBlockHeight + inputMetaHeight : 2;
|
|
582
1454
|
const topSeparatorHeight = nativePaneChrome ? 0 : 1;
|
|
583
1455
|
const footerSeparatorHeight = !nativePaneChrome && !canSend ? 1 : 0;
|
|
584
|
-
const messageAreaHeight = Math.max(1, height - topSeparatorHeight - footerSeparatorHeight - inputAreaHeight);
|
|
585
|
-
const composerWidth = nativePaneChrome ?
|
|
1456
|
+
const messageAreaHeight = Math.max(1, height - compactChannelHeaderHeight - topSeparatorHeight - footerSeparatorHeight - inputAreaHeight);
|
|
1457
|
+
const composerWidth = nativePaneChrome ? chatWidth : contentWidth;
|
|
586
1458
|
|
|
587
1459
|
useEffect(() => {
|
|
588
1460
|
if (selectedIdx < messages.length) return;
|
|
@@ -591,8 +1463,8 @@ export function ChatContent({
|
|
|
591
1463
|
|
|
592
1464
|
useEffect(() => {
|
|
593
1465
|
if (!stickyTranscript) return;
|
|
594
|
-
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
595
|
-
}, [contentWidth, height, messages, messageAreaHeight, stickyTranscript]);
|
|
1466
|
+
queueMicrotask(() => scrollToBottom(scrollRef.current, nativePaneChrome));
|
|
1467
|
+
}, [contentWidth, height, messages, messageAreaHeight, nativePaneChrome, stickyTranscript]);
|
|
596
1468
|
|
|
597
1469
|
useEffect(() => {
|
|
598
1470
|
const anchor = prependAnchorRef.current;
|
|
@@ -602,14 +1474,19 @@ export function ChatContent({
|
|
|
602
1474
|
const currentAnchor = prependAnchorRef.current;
|
|
603
1475
|
const scrollBox = scrollRef.current;
|
|
604
1476
|
if (!currentAnchor || !scrollBox) return;
|
|
1477
|
+
if (pendingJumpMessageIdRef.current) {
|
|
1478
|
+
prependAnchorRef.current = null;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
605
1481
|
|
|
606
1482
|
const previousOldestIndex = currentAnchor.oldestMessageId
|
|
607
1483
|
? messages.findIndex((message) => message.id === currentAnchor.oldestMessageId)
|
|
608
1484
|
: -1;
|
|
609
|
-
const
|
|
1485
|
+
const exactPixels = nativePaneChrome === true && hasPixelScrollMetrics(scrollBox);
|
|
1486
|
+
const addedRows = !exactPixels && previousOldestIndex > 0
|
|
610
1487
|
? getMessageTopOffset(messages, previousOldestIndex, contentWidth)
|
|
611
|
-
: Math.max(0, scrollBox
|
|
612
|
-
scrollBox
|
|
1488
|
+
: Math.max(0, getScrollHeight(scrollBox, exactPixels) - currentAnchor.scrollHeight);
|
|
1489
|
+
scrollToPosition(scrollBox, currentAnchor.scrollTop + addedRows, exactPixels);
|
|
613
1490
|
if (currentAnchor.selectedMessageId) {
|
|
614
1491
|
const selectedMessageIndex = messages.findIndex((message) => message.id === currentAnchor.selectedMessageId);
|
|
615
1492
|
if (selectedMessageIndex >= 0) {
|
|
@@ -618,29 +1495,34 @@ export function ChatContent({
|
|
|
618
1495
|
}
|
|
619
1496
|
prependAnchorRef.current = null;
|
|
620
1497
|
});
|
|
621
|
-
}, [contentWidth, loadingOlderMessages, messages]);
|
|
1498
|
+
}, [contentWidth, loadingOlderMessages, messages, nativePaneChrome]);
|
|
622
1499
|
|
|
623
1500
|
useEffect(() => {
|
|
624
1501
|
if (!focused || !stickyTranscript) return;
|
|
625
|
-
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
626
|
-
}, [focused, stickyTranscript]);
|
|
1502
|
+
queueMicrotask(() => scrollToBottom(scrollRef.current, nativePaneChrome));
|
|
1503
|
+
}, [focused, nativePaneChrome, stickyTranscript]);
|
|
627
1504
|
|
|
628
1505
|
useEffect(() => {
|
|
629
1506
|
const sb = scrollRef.current;
|
|
630
1507
|
if (!sb) return;
|
|
631
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
|
+
}
|
|
632
1514
|
const top = getMessageTopOffset(messages, selectedIdx, contentWidth);
|
|
633
1515
|
const rowHeight = estimateMessageHeight(messages[selectedIdx]!, contentWidth, isGroupedWithPrevious(messages, selectedIdx));
|
|
634
1516
|
const nextScrollTop = getSelectedMessageScrollTop({
|
|
635
1517
|
scrollTop: sb.scrollTop,
|
|
636
|
-
viewportHeight: sb.viewport
|
|
1518
|
+
viewportHeight: sb.viewport?.height ?? 0,
|
|
637
1519
|
top,
|
|
638
1520
|
rowHeight,
|
|
639
1521
|
});
|
|
640
1522
|
if (nextScrollTop !== sb.scrollTop) {
|
|
641
1523
|
sb.scrollTo(nextScrollTop);
|
|
642
1524
|
}
|
|
643
|
-
}, [contentWidth, messages, selectedIdx, selectionActive]);
|
|
1525
|
+
}, [contentWidth, messages, nativePaneChrome, selectedIdx, selectionActive]);
|
|
644
1526
|
|
|
645
1527
|
const replyPreview = replyTo
|
|
646
1528
|
? formatInlinePreview(
|
|
@@ -649,17 +1531,54 @@ export function ChatContent({
|
|
|
649
1531
|
)
|
|
650
1532
|
: "";
|
|
651
1533
|
const inputPlaceholder = replyTo ? `Reply to @${replyTo.user.username}...` : "Type a message...";
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
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;
|
|
660
1541
|
|
|
661
1542
|
return (
|
|
662
|
-
<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
|
+
|
|
663
1582
|
{!nativePaneChrome && (
|
|
664
1583
|
<Box height={1} width={contentWidth}>
|
|
665
1584
|
<Text fg={colors.border}>{"-".repeat(contentWidth)}</Text>
|
|
@@ -668,155 +1587,64 @@ export function ChatContent({
|
|
|
668
1587
|
|
|
669
1588
|
<ScrollBox
|
|
670
1589
|
ref={scrollRef}
|
|
671
|
-
height={messageAreaHeight}
|
|
1590
|
+
height={nativePaneChrome ? undefined : messageAreaHeight}
|
|
1591
|
+
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
672
1592
|
scrollY
|
|
673
1593
|
focusable={false}
|
|
674
1594
|
stickyScroll={stickyTranscript}
|
|
675
1595
|
stickyStart="bottom"
|
|
676
1596
|
onMouseScroll={() => queueMicrotask(requestOlderMessagesIfNeeded)}
|
|
677
|
-
style={
|
|
1597
|
+
style={nativePaneChrome ? { minHeight: 0 } : undefined}
|
|
678
1598
|
>
|
|
679
1599
|
{loadingOlderMessages && (
|
|
680
1600
|
<Box alignItems="center" justifyContent="center" height={1} width={contentWidth}>
|
|
681
1601
|
<Text fg={colors.textDim}>Loading earlier messages...</Text>
|
|
682
1602
|
</Box>
|
|
683
1603
|
)}
|
|
684
|
-
{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 && (
|
|
685
1609
|
<Box alignItems="center" justifyContent="center" flexGrow={1}>
|
|
686
1610
|
<Text fg={colors.textDim}>No messages yet. Be the first to say something!</Text>
|
|
687
1611
|
</Box>
|
|
688
1612
|
)}
|
|
689
|
-
{messages.map((msg, index) =>
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const showReplyAction = canSend && (isSelected || hoveredIdx === index);
|
|
693
|
-
const bgColor = isSelected ? colors.selected : isHovered ? hoverBg() : undefined;
|
|
694
|
-
const grouped = isGroupedWithPrevious(messages, index);
|
|
695
|
-
const isSending = msg.clientStatus === "sending";
|
|
696
|
-
const hasFailed = msg.clientStatus === "failed";
|
|
697
|
-
const selectedTextColor = hasFailed ? colors.negative : colors.selectedText;
|
|
698
|
-
const replyMetaColor = isSelected ? selectedTextColor : colors.textMuted;
|
|
699
|
-
const replyAuthorColor = isSelected ? selectedTextColor : colors.textDim;
|
|
700
|
-
const headerStatus = isSending ? "sending..." : hasFailed ? "failed" : formatTimeAgo(msg.createdAt);
|
|
701
|
-
const headerStatusColor = isSelected
|
|
702
|
-
? selectedTextColor
|
|
703
|
-
: isSending
|
|
704
|
-
? colors.textDim
|
|
705
|
-
: hasFailed
|
|
706
|
-
? colors.negative
|
|
707
|
-
: colors.textMuted;
|
|
708
|
-
const authorColor = isSelected ? selectedTextColor : hasFailed ? colors.negative : colors.positive;
|
|
709
|
-
const authorAttributes = (isSending ? TextAttributes.DIM : 0) | TextAttributes.BOLD;
|
|
710
|
-
const bodyColor = isSelected ? selectedTextColor : hasFailed ? colors.negative : isSending ? colors.textDim : colors.text;
|
|
711
|
-
const bodyLines = getMessageBodyLines(msg, contentWidth);
|
|
712
|
-
const showInlineReplyAction = !grouped && (nativePaneChrome ? canSend : showReplyAction);
|
|
713
|
-
const showGroupedReplyAction = grouped && (nativePaneChrome ? canSend : showReplyAction);
|
|
714
|
-
const setHovered = () => setHoveredIdx((current) => (current === index ? current : index));
|
|
715
|
-
const clearHovered = () => setHoveredIdx((current) => (current === index ? null : current));
|
|
716
|
-
const messageRowProps = {
|
|
717
|
-
width: contentWidth,
|
|
718
|
-
backgroundColor: bgColor,
|
|
719
|
-
"data-gloom-role": nativePaneChrome ? "chat-message-row" : undefined,
|
|
720
|
-
"data-selected": nativePaneChrome ? (isSelected ? "true" : "false") : undefined,
|
|
721
|
-
onMouseMove: nativePaneChrome ? undefined : setHovered,
|
|
722
|
-
onMouseOut: nativePaneChrome ? undefined : clearHovered,
|
|
723
|
-
};
|
|
724
|
-
return (
|
|
725
|
-
<Box
|
|
1613
|
+
{messages.map((msg, index) => (
|
|
1614
|
+
nativePaneChrome ? (
|
|
1615
|
+
<DesktopChatMessage
|
|
726
1616
|
key={msg.id}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
{
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
{
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
{msg.user.username ?? "anon"}
|
|
759
|
-
</Text>
|
|
760
|
-
<Text fg={headerStatusColor}> {headerStatus}</Text>
|
|
761
|
-
{showInlineReplyAction && (
|
|
762
|
-
<>
|
|
763
|
-
<Text fg={headerStatusColor}> </Text>
|
|
764
|
-
<Box
|
|
765
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
766
|
-
height={1}
|
|
767
|
-
data-gloom-role={nativePaneChrome ? "chat-message-reply-action" : undefined}
|
|
768
|
-
>
|
|
769
|
-
<ChatActionChip
|
|
770
|
-
label="Reply"
|
|
771
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
772
|
-
emphasized={isSelected}
|
|
773
|
-
onPress={() => beginReplyTo(index)}
|
|
774
|
-
/>
|
|
775
|
-
</Box>
|
|
776
|
-
</>
|
|
777
|
-
)}
|
|
778
|
-
</Box>
|
|
779
|
-
)}
|
|
780
|
-
{bodyLines.map((line, lineIndex) => (
|
|
781
|
-
<Box
|
|
782
|
-
key={`${msg.id}:body:${lineIndex}`}
|
|
783
|
-
{...messageRowProps}
|
|
784
|
-
paddingLeft={3}
|
|
785
|
-
height={1}
|
|
786
|
-
flexDirection="row"
|
|
787
|
-
position={grouped ? "relative" : undefined}
|
|
788
|
-
>
|
|
789
|
-
<Box width={messageBodyWidth} height={1}>
|
|
790
|
-
<TickerBadgeText
|
|
791
|
-
text={line}
|
|
792
|
-
lineWidth={messageBodyWidth}
|
|
793
|
-
catalog={catalog}
|
|
794
|
-
textColor={bodyColor}
|
|
795
|
-
openTicker={openTicker}
|
|
796
|
-
/>
|
|
797
|
-
</Box>
|
|
798
|
-
{lineIndex === 0 && showGroupedReplyAction && (
|
|
799
|
-
<Box
|
|
800
|
-
position="absolute"
|
|
801
|
-
top={0}
|
|
802
|
-
right={0}
|
|
803
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
804
|
-
height={1}
|
|
805
|
-
data-gloom-role={nativePaneChrome ? "chat-message-reply-action" : undefined}
|
|
806
|
-
>
|
|
807
|
-
<ChatActionChip
|
|
808
|
-
label="Reply"
|
|
809
|
-
width={MESSAGE_ACTION_WIDTH}
|
|
810
|
-
emphasized={isSelected}
|
|
811
|
-
onPress={() => beginReplyTo(index)}
|
|
812
|
-
/>
|
|
813
|
-
</Box>
|
|
814
|
-
)}
|
|
815
|
-
</Box>
|
|
816
|
-
))}
|
|
817
|
-
</Box>
|
|
818
|
-
);
|
|
819
|
-
})}
|
|
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
|
+
))}
|
|
820
1648
|
</ScrollBox>
|
|
821
1649
|
|
|
822
1650
|
{!nativePaneChrome && !canSend && (
|
|
@@ -887,17 +1715,79 @@ export function ChatContent({
|
|
|
887
1715
|
)}
|
|
888
1716
|
</Box>
|
|
889
1717
|
)}
|
|
1718
|
+
</Box>
|
|
890
1719
|
</Box>
|
|
891
1720
|
);
|
|
892
1721
|
}
|
|
893
1722
|
|
|
894
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
|
+
|
|
895
1783
|
return (
|
|
896
1784
|
<ChatContent
|
|
897
1785
|
width={width}
|
|
898
1786
|
height={height}
|
|
899
1787
|
focused={focused}
|
|
900
1788
|
close={close}
|
|
1789
|
+
channelId={channelId}
|
|
1790
|
+
onChannelChange={setChannelId}
|
|
901
1791
|
/>
|
|
902
1792
|
);
|
|
903
1793
|
}
|
|
@@ -977,8 +1867,16 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
977
1867
|
label: "New Chat Pane",
|
|
978
1868
|
description: "Open another floating chat window",
|
|
979
1869
|
keywords: ["new", "chat", "pane", "message"],
|
|
980
|
-
shortcut: { prefix: "CHAT" },
|
|
981
|
-
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
|
+
},
|
|
982
1880
|
},
|
|
983
1881
|
],
|
|
984
1882
|
|