gloomberb 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -54
- package/package.json +14 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/app/global-shortcuts.test.tsx +54 -4
- package/src/app/global-shortcuts.ts +11 -5
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- package/src/components/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +17 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +115 -86
- package/src/components/ui/ui.test.tsx +83 -4
- package/src/core/state/app/types.ts +12 -4
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +66 -4
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +326 -164
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/builtin/world-indices/index.tsx +9 -6
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/react/use-throttled-commit-value.ts +116 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/tickers/search/index.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChatChannel } from "../../../api-client";
|
|
2
|
+
import { normalizeChannelId } from "./controller/state";
|
|
3
|
+
|
|
4
|
+
function normalizeMentionUsername(username: string | null | undefined): string | null {
|
|
5
|
+
const normalized = username?.trim().replace(/^@+/, "");
|
|
6
|
+
return normalized || null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatMentionUsername(username: string | null | undefined): string | null {
|
|
10
|
+
const normalized = normalizeMentionUsername(username);
|
|
11
|
+
return normalized ? `@${normalized}` : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatDirectChannelLabel(channel: ChatChannel, fallbackId: string) {
|
|
15
|
+
const usernameLabel = formatMentionUsername(channel.dmUser?.username);
|
|
16
|
+
if (usernameLabel) return usernameLabel;
|
|
17
|
+
|
|
18
|
+
const displayName = channel.dmUser?.displayName?.trim();
|
|
19
|
+
if (displayName) return displayName;
|
|
20
|
+
|
|
21
|
+
const channelName = channel.name?.trim();
|
|
22
|
+
if (channelName && channelName !== channel.id) {
|
|
23
|
+
return channelName.startsWith("@")
|
|
24
|
+
? formatMentionUsername(channelName) ?? channelName
|
|
25
|
+
: channelName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return fallbackId.startsWith("dm:") ? "DM" : fallbackId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatChannelLabel(channel: ChatChannel | undefined, fallbackId: string) {
|
|
32
|
+
if (!channel) return fallbackId;
|
|
33
|
+
if (channel.kind === "direct") {
|
|
34
|
+
return formatDirectChannelLabel(channel, fallbackId);
|
|
35
|
+
}
|
|
36
|
+
if (channel.kind === "group") {
|
|
37
|
+
return channel.name?.trim() || "Group";
|
|
38
|
+
}
|
|
39
|
+
return channel.name?.trim() || fallbackId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatChatPaneTitle(channel: ChatChannel | undefined, fallbackId: string) {
|
|
43
|
+
const normalizedFallbackId = normalizeChannelId(fallbackId);
|
|
44
|
+
if (channel?.kind === "direct") {
|
|
45
|
+
return formatChannelLabel(channel, normalizedFallbackId);
|
|
46
|
+
}
|
|
47
|
+
if (channel?.kind === "group") {
|
|
48
|
+
return formatChannelLabel(channel, normalizedFallbackId);
|
|
49
|
+
}
|
|
50
|
+
if (!channel && normalizedFallbackId.startsWith("dm:")) return "DM";
|
|
51
|
+
if (!channel && (normalizedFallbackId.startsWith("grp:") || normalizedFallbackId.startsWith("group:"))) return "Group";
|
|
52
|
+
return `#${formatChannelLabel(channel, normalizedFallbackId)}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ChatChannel } from "../../../api-client";
|
|
3
|
+
import {
|
|
4
|
+
channelPrefix,
|
|
5
|
+
formatChannelLabel,
|
|
6
|
+
formatChatPaneTitle,
|
|
7
|
+
} from "./channels";
|
|
8
|
+
|
|
9
|
+
describe("chat channel labels", () => {
|
|
10
|
+
test("formats direct-message labels and titles with one mention prefix", () => {
|
|
11
|
+
const channel: ChatChannel = {
|
|
12
|
+
id: "dm:c7f89ce26f9da83d6be08a4838738074",
|
|
13
|
+
name: "@risto",
|
|
14
|
+
kind: "direct",
|
|
15
|
+
created_at: "2026-05-27T10:30:03.712Z",
|
|
16
|
+
dmUser: { id: "u2", username: "@risto", displayName: "Risto" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
expect(formatChannelLabel(channel, channel.id)).toBe("@risto");
|
|
20
|
+
expect(channelPrefix(channel, true)).toBe(" ");
|
|
21
|
+
expect(formatChatPaneTitle(channel, channel.id)).toBe("@risto");
|
|
22
|
+
expect(formatChatPaneTitle(undefined, channel.id)).toBe("DM");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("uses a neutral group title before private channel metadata loads", () => {
|
|
26
|
+
expect(formatChatPaneTitle(undefined, "grp:93f14c6b9827c30f")).toBe("Group");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { CommandResultDef, GloomPluginContext } from "../../../types/plugin";
|
|
2
|
-
import type { ChatChannel } from "../../../api-client";
|
|
2
|
+
import type { ChatChannel, ChatChannelState } from "../../../api-client";
|
|
3
|
+
import type { AppConfig, PaneInstanceConfig } from "../../../types/config";
|
|
3
4
|
import { chatController } from "./controller";
|
|
5
|
+
import { formatChannelLabel } from "./channel-labels";
|
|
6
|
+
|
|
7
|
+
export { formatChannelLabel, formatChatPaneTitle } from "./channel-labels";
|
|
4
8
|
|
|
5
9
|
export const DEFAULT_CHAT_CHANNEL_ID = "everyone";
|
|
6
10
|
export const LAST_VISITED_CHAT_CHANNEL_KEY = "lastChatChannelId";
|
|
@@ -21,15 +25,28 @@ export function getLastVisitedChatChannelId(config: { pluginConfig: Record<strin
|
|
|
21
25
|
return normalizeChannelId(config.pluginConfig["gloomberb-cloud"]?.[LAST_VISITED_CHAT_CHANNEL_KEY] as string | undefined);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
export function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
export function findChatPaneInstanceForChannel(config: AppConfig, channelId: string): PaneInstanceConfig | undefined {
|
|
29
|
+
const normalizedChannelId = normalizeChannelId(channelId);
|
|
30
|
+
return config.layout.instances.find((instance) => (
|
|
31
|
+
instance.paneId === "chat"
|
|
32
|
+
&& normalizeChannelId(typeof instance.settings?.channelId === "string" ? instance.settings.channelId : null) === normalizedChannelId
|
|
33
|
+
));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getPreferredChatOpenChannelId(
|
|
37
|
+
config: AppConfig,
|
|
38
|
+
snapshot?: { channels: ChatChannel[]; channelStates: ChatChannelState[] },
|
|
39
|
+
) {
|
|
40
|
+
if (!snapshot) return getLastVisitedChatChannelId(config);
|
|
41
|
+
const channelById = new Map(snapshot.channels.map((channel) => [channel.id, channel]));
|
|
42
|
+
const unreadStates = snapshot.channelStates
|
|
43
|
+
.filter((state) => state.unreadCount > 0 && channelById.has(state.channelId))
|
|
44
|
+
.sort((a, b) => b.unreadCount - a.unreadCount);
|
|
45
|
+
const unreadConversation = unreadStates.find((state) => {
|
|
46
|
+
const kind = channelById.get(state.channelId)?.kind ?? "public";
|
|
47
|
+
return kind === "direct" || kind === "group";
|
|
48
|
+
});
|
|
49
|
+
return normalizeChannelId(unreadConversation?.channelId ?? unreadStates[0]?.channelId ?? getLastVisitedChatChannelId(config));
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
function conversationDetail(channel: ChatChannel): string {
|
|
@@ -63,20 +80,20 @@ export function hasOnlyDmUsernameArgs(value: string): boolean {
|
|
|
63
80
|
return parts.length > 0 && parts.every((part) => CHAT_USERNAME_ARG.test(part));
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
function openChatChannelFromCommand(ctx: GloomPluginContext, channelId: string): void {
|
|
83
|
+
function openChatChannelFromCommand(ctx: Pick<GloomPluginContext, "createPaneFromTemplate" | "focusPane" | "getConfig">, channelId: string): void {
|
|
84
|
+
const existingInstance = findChatPaneInstanceForChannel(ctx.getConfig(), channelId);
|
|
85
|
+
if (existingInstance) {
|
|
86
|
+
ctx.focusPane(existingInstance.instanceId);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
67
89
|
ctx.createPaneFromTemplate("new-chat-pane", { arg: channelId });
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
export function openDefaultChatFromCommand(
|
|
71
|
-
ctx: Pick<GloomPluginContext, "createPaneFromTemplate" | "
|
|
93
|
+
ctx: Pick<GloomPluginContext, "createPaneFromTemplate" | "focusPane" | "getConfig">,
|
|
72
94
|
): void {
|
|
73
95
|
const config = ctx.getConfig();
|
|
74
|
-
|
|
75
|
-
if (hasChatPane) {
|
|
76
|
-
ctx.showPane("chat");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
ctx.createPaneFromTemplate("new-chat-pane", { arg: getLastVisitedChatChannelId(config) });
|
|
96
|
+
openChatChannelFromCommand(ctx, getPreferredChatOpenChannelId(config, chatController.getSnapshot()));
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
export async function openDmTargetFromCommand(ctx: GloomPluginContext, usernames: string[]): Promise<void> {
|
|
@@ -143,7 +160,7 @@ export function buildDmCommandResults(ctx: GloomPluginContext, arg: string): Com
|
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
export function channelPrefix(channel: ChatChannel | undefined, active: boolean) {
|
|
146
|
-
if (channel?.kind === "direct") return
|
|
163
|
+
if (channel?.kind === "direct") return " ";
|
|
147
164
|
if (channel?.kind === "group") return active ? "+" : " ";
|
|
148
165
|
return active ? "#" : " ";
|
|
149
166
|
}
|
|
@@ -9,7 +9,7 @@ import type { ChatMessage } from "../../../api-client";
|
|
|
9
9
|
import { apiClient } from "../../../api-client";
|
|
10
10
|
import { PluginRenderProvider } from "../../runtime";
|
|
11
11
|
import { setSharedRegistryForTests } from "../../registry";
|
|
12
|
-
import { ChatStatusWidget } from ".";
|
|
12
|
+
import { ChatContent, ChatStatusWidget } from ".";
|
|
13
13
|
import {
|
|
14
14
|
cleanupChatTest,
|
|
15
15
|
createChatTestControls,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createHarness,
|
|
18
18
|
hexToRgbaInts,
|
|
19
19
|
installChatApiTestDefaults,
|
|
20
|
+
installServerChannels,
|
|
20
21
|
lineText,
|
|
21
22
|
makeMessage,
|
|
22
23
|
type ChatTestSetup,
|
|
@@ -29,6 +30,21 @@ function setup(): ChatTestSetup {
|
|
|
29
30
|
}
|
|
30
31
|
const { flushFrame, emitKeypress } = createChatTestControls(setup);
|
|
31
32
|
|
|
33
|
+
function recentChatTimestamp(offsetMs = 60_000) {
|
|
34
|
+
return new Date(Date.now() - offsetMs).toISOString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeOwnMessage(content = "typo", createdAt = recentChatTimestamp()): ChatMessage {
|
|
38
|
+
return {
|
|
39
|
+
id: "m-own",
|
|
40
|
+
channelId: "everyone",
|
|
41
|
+
content,
|
|
42
|
+
replyToId: null,
|
|
43
|
+
createdAt,
|
|
44
|
+
user: { id: "u0", username: "vince", displayName: "Vince" },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
beforeEach(() => {
|
|
33
49
|
installChatApiTestDefaults();
|
|
34
50
|
});
|
|
@@ -39,6 +55,42 @@ afterEach(async () => {
|
|
|
39
55
|
});
|
|
40
56
|
|
|
41
57
|
describe("ChatContent", () => {
|
|
58
|
+
test("keeps a persisted DM selected while private channels refresh", async () => {
|
|
59
|
+
const controller = createController();
|
|
60
|
+
const dmChannelId = "dm:test";
|
|
61
|
+
installServerChannels(controller, [
|
|
62
|
+
{ id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
|
|
63
|
+
]);
|
|
64
|
+
controller.refreshChannels = async () => {};
|
|
65
|
+
controller.refreshPresence = async () => {};
|
|
66
|
+
controller.refreshSession = async () => {};
|
|
67
|
+
controller.refreshChannelMessages = async () => {};
|
|
68
|
+
const channelChanges: string[] = [];
|
|
69
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
70
|
+
|
|
71
|
+
await act(async () => {
|
|
72
|
+
testSetup = await testRender(
|
|
73
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
74
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
75
|
+
<ChatContent
|
|
76
|
+
controller={controller}
|
|
77
|
+
width={60}
|
|
78
|
+
height={12}
|
|
79
|
+
focused
|
|
80
|
+
channelId={dmChannelId}
|
|
81
|
+
onChannelChange={(nextChannelId) => channelChanges.push(nextChannelId)}
|
|
82
|
+
/>
|
|
83
|
+
</PluginRenderProvider>
|
|
84
|
+
</AppContext>,
|
|
85
|
+
{ width: 60, height: 12 },
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await flushFrame();
|
|
90
|
+
|
|
91
|
+
expect(channelChanges).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
42
94
|
test("focuses the prompt on click and preserves typing order", async () => {
|
|
43
95
|
const controller = createController();
|
|
44
96
|
|
|
@@ -179,6 +231,108 @@ describe("ChatContent", () => {
|
|
|
179
231
|
expect(bodyLine).not.toContain("Reply");
|
|
180
232
|
});
|
|
181
233
|
|
|
234
|
+
test("shows the edit action next to reply for the latest own message", async () => {
|
|
235
|
+
const ownMessage = makeOwnMessage();
|
|
236
|
+
const controller = createController({
|
|
237
|
+
messages: [makeMessage(1), ownMessage],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await act(async () => {
|
|
241
|
+
testSetup = await testRender(createHarness(controller, {
|
|
242
|
+
width: 72,
|
|
243
|
+
height: 12,
|
|
244
|
+
}), {
|
|
245
|
+
width: 72,
|
|
246
|
+
height: 12,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await flushFrame();
|
|
251
|
+
|
|
252
|
+
await emitKeypress({ name: "up", sequence: "\u001b[A" });
|
|
253
|
+
await flushFrame();
|
|
254
|
+
|
|
255
|
+
const lines = setup().captureCharFrame().split("\n");
|
|
256
|
+
const headerLine = lines.find((line) => line.includes("vince"));
|
|
257
|
+
|
|
258
|
+
expect(headerLine).toContain("Reply");
|
|
259
|
+
expect(headerLine).toContain("Edit");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("hides the edit action after the edit window expires", async () => {
|
|
263
|
+
const ownMessage = makeOwnMessage("typo", recentChatTimestamp(16 * 60_000));
|
|
264
|
+
const controller = createController({
|
|
265
|
+
messages: [makeMessage(1), ownMessage],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await act(async () => {
|
|
269
|
+
testSetup = await testRender(createHarness(controller, {
|
|
270
|
+
width: 72,
|
|
271
|
+
height: 12,
|
|
272
|
+
}), {
|
|
273
|
+
width: 72,
|
|
274
|
+
height: 12,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await flushFrame();
|
|
279
|
+
|
|
280
|
+
await emitKeypress({ name: "up", sequence: "\u001b[A" });
|
|
281
|
+
await flushFrame();
|
|
282
|
+
|
|
283
|
+
const lines = setup().captureCharFrame().split("\n");
|
|
284
|
+
const headerLine = lines.find((line) => line.includes("vince"));
|
|
285
|
+
|
|
286
|
+
expect(headerLine).toContain("Reply");
|
|
287
|
+
expect(headerLine).not.toContain("Edit");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("up arrow from an empty focused composer edits the latest own message", async () => {
|
|
291
|
+
const ownMessage = makeOwnMessage();
|
|
292
|
+
const controller = createController({
|
|
293
|
+
messages: [makeMessage(1), ownMessage],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
testSetup = await testRender(createHarness(controller, {
|
|
298
|
+
width: 72,
|
|
299
|
+
height: 12,
|
|
300
|
+
}), {
|
|
301
|
+
width: 72,
|
|
302
|
+
height: 12,
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await flushFrame();
|
|
307
|
+
|
|
308
|
+
const lines = setup().captureCharFrame().split("\n");
|
|
309
|
+
const inputRow = lines.findIndex((line) => line.includes("Type a message..."));
|
|
310
|
+
const inputCol = lines[inputRow]?.indexOf("Type a message...") ?? -1;
|
|
311
|
+
expect(inputRow).toBeGreaterThanOrEqual(0);
|
|
312
|
+
expect(inputCol).toBeGreaterThanOrEqual(0);
|
|
313
|
+
|
|
314
|
+
await act(async () => {
|
|
315
|
+
await setup().mockMouse.click(inputCol + 1, inputRow);
|
|
316
|
+
await setup().renderOnce();
|
|
317
|
+
await setup().renderOnce();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await emitKeypress({ name: "up", sequence: "\u001b[A" });
|
|
321
|
+
await flushFrame();
|
|
322
|
+
|
|
323
|
+
const frame = setup().captureCharFrame();
|
|
324
|
+
expect(frame).toContain("editing");
|
|
325
|
+
expect(frame).toContain("> typo");
|
|
326
|
+
|
|
327
|
+
await act(async () => {
|
|
328
|
+
await setup().mockInput.typeText(" fix");
|
|
329
|
+
await setup().renderOnce();
|
|
330
|
+
await setup().renderOnce();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(setup().captureCharFrame()).toContain("> typo fix");
|
|
334
|
+
});
|
|
335
|
+
|
|
182
336
|
test("down arrow from the newest selected message returns focus to the composer", async () => {
|
|
183
337
|
const controller = createController({
|
|
184
338
|
messages: [makeMessage(1), makeMessage(2)],
|
|
@@ -867,6 +1021,36 @@ describe("ChatContent", () => {
|
|
|
867
1021
|
expect(frame).toContain("https://example.com/story.");
|
|
868
1022
|
});
|
|
869
1023
|
|
|
1024
|
+
test("keeps long link query strings from spilling into following terminal rows", async () => {
|
|
1025
|
+
const controller = createController({
|
|
1026
|
+
messages: [{
|
|
1027
|
+
id: "m1",
|
|
1028
|
+
channelId: "everyone",
|
|
1029
|
+
content: "again:\nhttps://github.com/houmain/keymapper/issues?weird_one=the_rest_of the query parameters got lost entirely :)",
|
|
1030
|
+
replyToId: null,
|
|
1031
|
+
createdAt: "2026-03-28T00:00:00.000Z",
|
|
1032
|
+
user: { id: "u1", username: "vince", displayName: "Vince" },
|
|
1033
|
+
}],
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
await act(async () => {
|
|
1037
|
+
testSetup = await testRender(createHarness(controller, {
|
|
1038
|
+
width: 60,
|
|
1039
|
+
height: 12,
|
|
1040
|
+
}), {
|
|
1041
|
+
width: 60,
|
|
1042
|
+
height: 12,
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
await flushFrame();
|
|
1047
|
+
|
|
1048
|
+
const frame = setup().captureCharFrame();
|
|
1049
|
+
expect(frame).toContain("https://github.com/houmain/keymapper/issues?weird_one=");
|
|
1050
|
+
expect(frame).toContain("the_rest_of the query parameters got lost entirely :)");
|
|
1051
|
+
expect(frame).not.toContain("therquery=parametersfgot");
|
|
1052
|
+
});
|
|
1053
|
+
|
|
870
1054
|
test("shows a saved-login read-only footer when a session token is cached", async () => {
|
|
871
1055
|
const controller = createController({
|
|
872
1056
|
sessionToken: "token-123",
|
|
@@ -993,13 +1177,17 @@ describe("ChatContent", () => {
|
|
|
993
1177
|
sessionToken: "token-123",
|
|
994
1178
|
user: { id: "u1", username: "vince", emailVerified: true },
|
|
995
1179
|
});
|
|
996
|
-
const
|
|
1180
|
+
const openedTemplates: Array<{ templateId: string; options?: { arg?: string } }> = [];
|
|
1181
|
+
const focusedPanes: string[] = [];
|
|
997
1182
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
998
1183
|
state.config.disabledPlugins = [];
|
|
999
1184
|
|
|
1000
1185
|
const runtime = createTestPluginRuntime({
|
|
1001
|
-
|
|
1002
|
-
|
|
1186
|
+
createPaneFromTemplate(templateId: string, options?: { arg?: string }) {
|
|
1187
|
+
openedTemplates.push({ templateId, options });
|
|
1188
|
+
},
|
|
1189
|
+
focusPane(paneId: string) {
|
|
1190
|
+
focusedPanes.push(paneId);
|
|
1003
1191
|
},
|
|
1004
1192
|
});
|
|
1005
1193
|
|
|
@@ -1042,7 +1230,78 @@ describe("ChatContent", () => {
|
|
|
1042
1230
|
await setup().renderOnce();
|
|
1043
1231
|
});
|
|
1044
1232
|
|
|
1045
|
-
expect(
|
|
1233
|
+
expect(openedTemplates).toEqual([]);
|
|
1234
|
+
expect(focusedPanes).toHaveLength(1);
|
|
1235
|
+
expect(focusedPanes[0]?.startsWith("chat:")).toBe(true);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("opens an unread direct-message channel from the status widget", async () => {
|
|
1239
|
+
const controller = createController({
|
|
1240
|
+
sessionToken: "token-123",
|
|
1241
|
+
user: { id: "u1", username: "vince", emailVerified: true },
|
|
1242
|
+
});
|
|
1243
|
+
const dmChannelId = "dm:test";
|
|
1244
|
+
installServerChannels(controller, [
|
|
1245
|
+
{ id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
|
|
1246
|
+
{
|
|
1247
|
+
id: dmChannelId,
|
|
1248
|
+
name: "@bob",
|
|
1249
|
+
kind: "direct",
|
|
1250
|
+
created_at: "2026-05-27T10:30:03.712Z",
|
|
1251
|
+
dmUser: { id: "u2", username: "bob", displayName: "Bob" },
|
|
1252
|
+
},
|
|
1253
|
+
]);
|
|
1254
|
+
const openedTemplates: Array<{ templateId: string; options?: { arg?: string } }> = [];
|
|
1255
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
1256
|
+
state.config.disabledPlugins = [];
|
|
1257
|
+
|
|
1258
|
+
const runtime = createTestPluginRuntime({
|
|
1259
|
+
createPaneFromTemplate(templateId: string, options?: { arg?: string }) {
|
|
1260
|
+
openedTemplates.push({ templateId, options });
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
await act(async () => {
|
|
1265
|
+
testSetup = await testRender(
|
|
1266
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
1267
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={runtime}>
|
|
1268
|
+
<ChatStatusWidget controller={controller} />
|
|
1269
|
+
</PluginRenderProvider>
|
|
1270
|
+
</AppContext>,
|
|
1271
|
+
{ width: 40, height: 1 },
|
|
1272
|
+
);
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
await flushFrame();
|
|
1276
|
+
|
|
1277
|
+
await act(async () => {
|
|
1278
|
+
(controller as any).mergeMessages(dmChannelId, [{
|
|
1279
|
+
id: "dm-m1",
|
|
1280
|
+
channelId: dmChannelId,
|
|
1281
|
+
content: "private ping",
|
|
1282
|
+
replyToId: null,
|
|
1283
|
+
createdAt: "2026-05-27T10:31:00.000Z",
|
|
1284
|
+
user: { id: "u2", username: "bob", displayName: "Bob" },
|
|
1285
|
+
} satisfies ChatMessage]);
|
|
1286
|
+
});
|
|
1287
|
+
await flushFrame();
|
|
1288
|
+
|
|
1289
|
+
const frame = setup().captureCharFrame();
|
|
1290
|
+
expect(frame).toContain("vince");
|
|
1291
|
+
expect(frame).toContain("[1]");
|
|
1292
|
+
|
|
1293
|
+
const line = frame.split("\n")[0] ?? "";
|
|
1294
|
+
const badgeCol = line.indexOf("[1]");
|
|
1295
|
+
|
|
1296
|
+
expect(badgeCol).toBeGreaterThanOrEqual(0);
|
|
1297
|
+
|
|
1298
|
+
await act(async () => {
|
|
1299
|
+
await setup().mockMouse.click(badgeCol + 1, 0);
|
|
1300
|
+
await setup().renderOnce();
|
|
1301
|
+
await setup().renderOnce();
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
expect(openedTemplates).toEqual([{ templateId: "new-chat-pane", options: { arg: dmChannelId } }]);
|
|
1046
1305
|
});
|
|
1047
1306
|
|
|
1048
1307
|
});
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
3
3
|
import type { ChatChannel, ChatUserSummary } from "../../../../api-client";
|
|
4
|
+
import { useThrottledCommitValue } from "../../../../react/use-throttled-commit-value";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_CHAT_CHANNEL_ID,
|
|
6
7
|
normalizeChannelId,
|
|
7
8
|
} from "../channels";
|
|
8
9
|
import type { ChatContentController } from "./types";
|
|
9
10
|
|
|
11
|
+
const CHANNEL_NAVIGATION_COMMIT_DELAY_MS = 150;
|
|
12
|
+
|
|
13
|
+
function isConversationChannelId(channelId: string): boolean {
|
|
14
|
+
return channelId.startsWith("dm:") || channelId.startsWith("grp:") || channelId.startsWith("group:");
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export function useChatChannelNavigation({
|
|
11
18
|
blurInput,
|
|
12
19
|
channelId,
|
|
@@ -34,15 +41,16 @@ export function useChatChannelNavigation({
|
|
|
34
41
|
resetTranscriptSelection: () => void;
|
|
35
42
|
showChannelSidebar: boolean;
|
|
36
43
|
}): {
|
|
37
|
-
changeChannel: (nextChannelId: string) => void;
|
|
38
44
|
cycleChannel: (direction: 1 | -1) => boolean;
|
|
39
45
|
directExpanded: boolean;
|
|
40
46
|
focusChannelSidebar: () => boolean;
|
|
41
47
|
focusChatContent: () => boolean;
|
|
42
48
|
moveSidebarChannelSelection: (direction: "up" | "down") => boolean;
|
|
43
49
|
openDirectMessage: (target: ChatUserSummary) => Promise<void>;
|
|
50
|
+
selectSidebarChannel: (channelId: string) => void;
|
|
44
51
|
setDirectExpanded: Dispatch<SetStateAction<boolean>>;
|
|
45
52
|
setSidebarFocused: (nextFocused: boolean) => void;
|
|
53
|
+
sidebarCursorChannelId: string;
|
|
46
54
|
sidebarFocused: boolean;
|
|
47
55
|
sidebarFocusedRef: MutableRefObject<boolean>;
|
|
48
56
|
} {
|
|
@@ -66,6 +74,13 @@ export function useChatChannelNavigation({
|
|
|
66
74
|
resetTranscriptSelection();
|
|
67
75
|
onChannelChange?.(normalized);
|
|
68
76
|
}, [channelIdRef, onChannelChange, resetTranscriptSelection]);
|
|
77
|
+
const {
|
|
78
|
+
value: sidebarCursorChannelId,
|
|
79
|
+
valueRef: sidebarCursorChannelIdRef,
|
|
80
|
+
setValue: setSidebarCursorChannelId,
|
|
81
|
+
flushValue: flushSidebarCursorChannelId,
|
|
82
|
+
replaceValue: replaceSidebarCursorChannelId,
|
|
83
|
+
} = useThrottledCommitValue(channelId, changeChannel, CHANNEL_NAVIGATION_COMMIT_DELAY_MS);
|
|
69
84
|
|
|
70
85
|
const openDirectMessage = useCallback(async (target: ChatUserSummary) => {
|
|
71
86
|
if (!target.id && !target.username) return;
|
|
@@ -75,14 +90,16 @@ export function useChatChannelNavigation({
|
|
|
75
90
|
username: target.username ?? undefined,
|
|
76
91
|
});
|
|
77
92
|
setDirectExpanded(true);
|
|
78
|
-
|
|
93
|
+
setSidebarCursorChannelId(channel.id, { immediate: true });
|
|
79
94
|
closeProfilePopover();
|
|
80
95
|
} catch {}
|
|
81
|
-
}, [
|
|
96
|
+
}, [closeProfilePopover, controller, setSidebarCursorChannelId]);
|
|
82
97
|
|
|
83
98
|
useEffect(() => {
|
|
84
99
|
if (!onChannelChange || channelsLoading || channels.length === 0) return;
|
|
85
100
|
if (channels.some((channel) => channel.id === channelId)) return;
|
|
101
|
+
// DM/group panes can remount before the private channel catalog is refreshed.
|
|
102
|
+
if (isConversationChannelId(channelId)) return;
|
|
86
103
|
const fallbackChannelId = channels.find((channel) => channel.id === DEFAULT_CHAT_CHANNEL_ID)?.id
|
|
87
104
|
?? channels[0]?.id;
|
|
88
105
|
if (fallbackChannelId) {
|
|
@@ -103,9 +120,9 @@ export function useChatChannelNavigation({
|
|
|
103
120
|
const nextIndex = (currentIndex + direction + channels.length) % channels.length;
|
|
104
121
|
const nextChannel = channels[nextIndex];
|
|
105
122
|
if (!nextChannel) return false;
|
|
106
|
-
|
|
123
|
+
setSidebarCursorChannelId(nextChannel.id, { immediate: true });
|
|
107
124
|
return true;
|
|
108
|
-
}, [
|
|
125
|
+
}, [channelIdRef, channels, onChannelChange, setSidebarCursorChannelId]);
|
|
109
126
|
|
|
110
127
|
const focusChannelSidebar = useCallback(() => {
|
|
111
128
|
if (!showChannelSidebar || !onChannelChange) return false;
|
|
@@ -113,28 +130,49 @@ export function useChatChannelNavigation({
|
|
|
113
130
|
blurInput();
|
|
114
131
|
}
|
|
115
132
|
resetTranscriptSelection();
|
|
133
|
+
replaceSidebarCursorChannelId(channelId);
|
|
116
134
|
setSidebarFocused(true);
|
|
117
135
|
return true;
|
|
118
|
-
}, [
|
|
136
|
+
}, [
|
|
137
|
+
blurInput,
|
|
138
|
+
channelId,
|
|
139
|
+
inputFocused,
|
|
140
|
+
onChannelChange,
|
|
141
|
+
replaceSidebarCursorChannelId,
|
|
142
|
+
resetTranscriptSelection,
|
|
143
|
+
setSidebarFocused,
|
|
144
|
+
showChannelSidebar,
|
|
145
|
+
]);
|
|
119
146
|
|
|
120
147
|
const focusChatContent = useCallback(() => {
|
|
121
148
|
if (!showChannelSidebar) return false;
|
|
149
|
+
flushSidebarCursorChannelId(sidebarCursorChannelIdRef.current);
|
|
122
150
|
setSidebarFocused(false);
|
|
123
151
|
return true;
|
|
124
|
-
}, [setSidebarFocused, showChannelSidebar]);
|
|
152
|
+
}, [flushSidebarCursorChannelId, setSidebarFocused, showChannelSidebar, sidebarCursorChannelIdRef]);
|
|
125
153
|
|
|
126
154
|
const moveSidebarChannelSelection = useCallback((direction: "up" | "down") => {
|
|
127
155
|
if (!showChannelSidebar || sidebarNavigationChannels.length <= 1 || !onChannelChange) return false;
|
|
128
|
-
const currentIndex = sidebarNavigationChannels.findIndex((channel) => channel.id ===
|
|
156
|
+
const currentIndex = sidebarNavigationChannels.findIndex((channel) => channel.id === sidebarCursorChannelIdRef.current);
|
|
129
157
|
const baseIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
130
158
|
const nextIndex = direction === "down"
|
|
131
159
|
? Math.min(baseIndex + 1, sidebarNavigationChannels.length - 1)
|
|
132
160
|
: Math.max(baseIndex - 1, 0);
|
|
133
161
|
const nextChannel = sidebarNavigationChannels[nextIndex];
|
|
134
162
|
if (!nextChannel || nextIndex === baseIndex) return true;
|
|
135
|
-
|
|
163
|
+
setSidebarCursorChannelId(nextChannel.id);
|
|
136
164
|
return true;
|
|
137
|
-
}, [
|
|
165
|
+
}, [
|
|
166
|
+
onChannelChange,
|
|
167
|
+
setSidebarCursorChannelId,
|
|
168
|
+
showChannelSidebar,
|
|
169
|
+
sidebarCursorChannelIdRef,
|
|
170
|
+
sidebarNavigationChannels,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const selectSidebarChannel = useCallback((nextChannelId: string) => {
|
|
174
|
+
setSidebarCursorChannelId(nextChannelId, { immediate: true });
|
|
175
|
+
}, [setSidebarCursorChannelId]);
|
|
138
176
|
|
|
139
177
|
useEffect(() => {
|
|
140
178
|
if (focused && showChannelSidebar) return;
|
|
@@ -142,15 +180,16 @@ export function useChatChannelNavigation({
|
|
|
142
180
|
}, [focused, setSidebarFocused, showChannelSidebar]);
|
|
143
181
|
|
|
144
182
|
return {
|
|
145
|
-
changeChannel,
|
|
146
183
|
cycleChannel,
|
|
147
184
|
directExpanded,
|
|
148
185
|
focusChannelSidebar,
|
|
149
186
|
focusChatContent,
|
|
150
187
|
moveSidebarChannelSelection,
|
|
151
188
|
openDirectMessage,
|
|
189
|
+
selectSidebarChannel,
|
|
152
190
|
setDirectExpanded,
|
|
153
191
|
setSidebarFocused,
|
|
192
|
+
sidebarCursorChannelId,
|
|
154
193
|
sidebarFocused,
|
|
155
194
|
sidebarFocusedRef,
|
|
156
195
|
};
|