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,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useRef } from "react";
|
|
3
|
+
import type { ChatMessage } from "../../../../api-client";
|
|
4
|
+
import { testRender } from "../../../../renderers/opentui/test-utils";
|
|
5
|
+
import { useChatComposerRuntime } from "./composer-runtime";
|
|
6
|
+
|
|
7
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
if (testSetup) {
|
|
11
|
+
await act(async () => {
|
|
12
|
+
testSetup?.renderer.destroy();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
testSetup = undefined;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("useChatComposerRuntime", () => {
|
|
19
|
+
test("sends to the pending channel ref instead of the stale rendered channel", async () => {
|
|
20
|
+
const sent: Array<{ channelId: string; content: string; replyToId?: string }> = [];
|
|
21
|
+
let sendMessage = () => {};
|
|
22
|
+
|
|
23
|
+
function Harness() {
|
|
24
|
+
const applyingExternalDraftRef = useRef(false);
|
|
25
|
+
const channelIdRef = useRef("dm:test");
|
|
26
|
+
const inputRef = useRef(null);
|
|
27
|
+
const inputValueRef = useRef("PM reply test #2");
|
|
28
|
+
sendMessage = useChatComposerRuntime({
|
|
29
|
+
applyingExternalDraftRef,
|
|
30
|
+
blurInput: () => {},
|
|
31
|
+
canSend: true,
|
|
32
|
+
channelId: "everyone",
|
|
33
|
+
channelIdRef,
|
|
34
|
+
contentWidth: 80,
|
|
35
|
+
controller: {
|
|
36
|
+
send: () => false,
|
|
37
|
+
sendToChannel: (channelId, content, replyToId) => {
|
|
38
|
+
sent.push({ channelId, content, replyToId });
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
openDirectChannel: async () => { throw new Error("not used"); },
|
|
42
|
+
openGroupChannel: async () => { throw new Error("not used"); },
|
|
43
|
+
setDraft: () => {},
|
|
44
|
+
setChannelDraft: () => {},
|
|
45
|
+
setReplyToId: () => {},
|
|
46
|
+
setChannelReplyToId: () => {},
|
|
47
|
+
} as any,
|
|
48
|
+
focusInput: () => {},
|
|
49
|
+
focused: true,
|
|
50
|
+
inputFocused: true,
|
|
51
|
+
inputRef,
|
|
52
|
+
inputValueRef,
|
|
53
|
+
messages: [],
|
|
54
|
+
onChannelChange: () => {},
|
|
55
|
+
replyTo: null,
|
|
56
|
+
setDirectExpanded: () => {},
|
|
57
|
+
setFollowMessages: () => {},
|
|
58
|
+
setReplyTo: () => {},
|
|
59
|
+
setSelectedIdx: () => {},
|
|
60
|
+
updateComposerRows: () => {},
|
|
61
|
+
useDefaultControllerChannel: false,
|
|
62
|
+
}).sendMessage;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
testSetup = await testRender(<Harness />, { width: 1, height: 1 });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await act(async () => {
|
|
71
|
+
sendMessage();
|
|
72
|
+
await testSetup?.renderOnce();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(sent).toEqual([{ channelId: "dm:test", content: "PM reply test #2", replyToId: undefined }]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("drops a reply target from a different channel before sending", async () => {
|
|
79
|
+
const sent: Array<{ channelId: string; content: string; replyToId?: string }> = [];
|
|
80
|
+
let sendMessage = () => {};
|
|
81
|
+
const staleReply: ChatMessage = {
|
|
82
|
+
id: "public-reply",
|
|
83
|
+
channelId: "everyone",
|
|
84
|
+
content: "public message",
|
|
85
|
+
replyToId: null,
|
|
86
|
+
createdAt: "2026-05-27T10:26:44.737Z",
|
|
87
|
+
user: { id: "u2", username: "vince", displayName: "Vince" },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function Harness() {
|
|
91
|
+
const applyingExternalDraftRef = useRef(false);
|
|
92
|
+
const channelIdRef = useRef("dm:test");
|
|
93
|
+
const inputRef = useRef(null);
|
|
94
|
+
const inputValueRef = useRef("replying in DM");
|
|
95
|
+
sendMessage = useChatComposerRuntime({
|
|
96
|
+
applyingExternalDraftRef,
|
|
97
|
+
blurInput: () => {},
|
|
98
|
+
canSend: true,
|
|
99
|
+
channelId: "dm:test",
|
|
100
|
+
channelIdRef,
|
|
101
|
+
contentWidth: 80,
|
|
102
|
+
controller: {
|
|
103
|
+
send: () => false,
|
|
104
|
+
sendToChannel: (channelId, content, replyToId) => {
|
|
105
|
+
sent.push({ channelId, content, replyToId });
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
openDirectChannel: async () => { throw new Error("not used"); },
|
|
109
|
+
openGroupChannel: async () => { throw new Error("not used"); },
|
|
110
|
+
setDraft: () => {},
|
|
111
|
+
setChannelDraft: () => {},
|
|
112
|
+
setReplyToId: () => {},
|
|
113
|
+
setChannelReplyToId: () => {},
|
|
114
|
+
} as any,
|
|
115
|
+
focusInput: () => {},
|
|
116
|
+
focused: true,
|
|
117
|
+
inputFocused: true,
|
|
118
|
+
inputRef,
|
|
119
|
+
inputValueRef,
|
|
120
|
+
messages: [],
|
|
121
|
+
onChannelChange: () => {},
|
|
122
|
+
replyTo: staleReply,
|
|
123
|
+
setDirectExpanded: () => {},
|
|
124
|
+
setFollowMessages: () => {},
|
|
125
|
+
setReplyTo: () => {},
|
|
126
|
+
setSelectedIdx: () => {},
|
|
127
|
+
updateComposerRows: () => {},
|
|
128
|
+
useDefaultControllerChannel: false,
|
|
129
|
+
}).sendMessage;
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await act(async () => {
|
|
134
|
+
testSetup = await testRender(<Harness />, { width: 1, height: 1 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await act(async () => {
|
|
138
|
+
sendMessage();
|
|
139
|
+
await testSetup?.renderOnce();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(sent).toEqual([{ channelId: "dm:test", content: "replying in DM", replyToId: undefined }]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -12,6 +12,26 @@ interface MutableRef<T> {
|
|
|
12
12
|
current: T;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function moveComposerCursorToEnd(textarea: TextareaRenderable, draft: string) {
|
|
16
|
+
const offset = draft.length;
|
|
17
|
+
const editBuffer = textarea.editBuffer as typeof textarea.editBuffer & {
|
|
18
|
+
setCursorByOffset?: (offset: number) => void;
|
|
19
|
+
};
|
|
20
|
+
if (typeof editBuffer.setCursorByOffset === "function") {
|
|
21
|
+
editBuffer.setCursorByOffset(offset);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (typeof textarea.setCursorOffset === "function") {
|
|
25
|
+
textarea.setCursorOffset(offset);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
textarea.cursorOffset = offset;
|
|
30
|
+
} catch {
|
|
31
|
+
// Some host renderers expose cursorOffset as read-only.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
export function useChatComposerRuntime({
|
|
16
36
|
applyingExternalDraftRef,
|
|
17
37
|
blurInput,
|
|
@@ -27,7 +47,10 @@ export function useChatComposerRuntime({
|
|
|
27
47
|
inputValueRef,
|
|
28
48
|
messages,
|
|
29
49
|
onChannelChange,
|
|
50
|
+
editingMessage,
|
|
51
|
+
latestEditableMessageId,
|
|
30
52
|
replyTo,
|
|
53
|
+
setEditingMessage,
|
|
31
54
|
setDirectExpanded,
|
|
32
55
|
setFollowMessages,
|
|
33
56
|
setReplyTo,
|
|
@@ -49,7 +72,10 @@ export function useChatComposerRuntime({
|
|
|
49
72
|
inputValueRef: MutableRef<string>;
|
|
50
73
|
messages: ChatMessage[];
|
|
51
74
|
onChannelChange?: (channelId: string) => void;
|
|
75
|
+
editingMessage: ChatMessage | null;
|
|
76
|
+
latestEditableMessageId: string | null;
|
|
52
77
|
replyTo: ChatMessage | null;
|
|
78
|
+
setEditingMessage: Dispatch<SetStateAction<ChatMessage | null>>;
|
|
53
79
|
setDirectExpanded: Dispatch<SetStateAction<boolean>>;
|
|
54
80
|
setFollowMessages: Dispatch<SetStateAction<boolean>>;
|
|
55
81
|
setReplyTo: Dispatch<SetStateAction<ChatMessage | null>>;
|
|
@@ -59,6 +85,9 @@ export function useChatComposerRuntime({
|
|
|
59
85
|
}) {
|
|
60
86
|
const replyToRef = useRef(replyTo);
|
|
61
87
|
replyToRef.current = replyTo;
|
|
88
|
+
const editingMessageRef = useRef(editingMessage);
|
|
89
|
+
editingMessageRef.current = editingMessage;
|
|
90
|
+
const editSubmittingRef = useRef(false);
|
|
62
91
|
|
|
63
92
|
const focusComposer = useCallback(() => {
|
|
64
93
|
setSelectedIdx(-1);
|
|
@@ -75,10 +104,40 @@ export function useChatComposerRuntime({
|
|
|
75
104
|
}
|
|
76
105
|
}, [channelId, controller, setReplyTo, useDefaultControllerChannel]);
|
|
77
106
|
|
|
107
|
+
const persistDraft = useCallback((draft: string) => {
|
|
108
|
+
if (useDefaultControllerChannel) {
|
|
109
|
+
controller.setDraft(draft);
|
|
110
|
+
} else {
|
|
111
|
+
controller.setChannelDraft(channelId, draft);
|
|
112
|
+
}
|
|
113
|
+
}, [channelId, controller, useDefaultControllerChannel]);
|
|
114
|
+
|
|
115
|
+
const replaceLocalComposer = useCallback((draft: string) => {
|
|
116
|
+
inputValueRef.current = draft;
|
|
117
|
+
updateComposerRows(draft);
|
|
118
|
+
const textarea = inputRef.current;
|
|
119
|
+
if (textarea && textarea.editBuffer.getText() !== draft) {
|
|
120
|
+
applyingExternalDraftRef.current = true;
|
|
121
|
+
try {
|
|
122
|
+
textarea.setText(draft);
|
|
123
|
+
moveComposerCursorToEnd(textarea, draft);
|
|
124
|
+
} finally {
|
|
125
|
+
applyingExternalDraftRef.current = false;
|
|
126
|
+
}
|
|
127
|
+
} else if (textarea) {
|
|
128
|
+
moveComposerCursorToEnd(textarea, draft);
|
|
129
|
+
}
|
|
130
|
+
}, [applyingExternalDraftRef, inputRef, inputValueRef, updateComposerRows]);
|
|
131
|
+
|
|
78
132
|
const beginReplyTo = useCallback((index: number, options?: { deferFocus?: boolean }) => {
|
|
79
133
|
if (!canSend || index < 0 || index >= messages.length) return;
|
|
80
134
|
const nextReplyTo = messages[index] ?? null;
|
|
81
135
|
if (!nextReplyTo) return;
|
|
136
|
+
if (editingMessageRef.current) {
|
|
137
|
+
replaceLocalComposer("");
|
|
138
|
+
persistDraft("");
|
|
139
|
+
}
|
|
140
|
+
setEditingMessage(null);
|
|
82
141
|
setSelectedIdx(index);
|
|
83
142
|
setFollowMessages(index === messages.length - 1);
|
|
84
143
|
setReplyTo(nextReplyTo);
|
|
@@ -98,12 +157,58 @@ export function useChatComposerRuntime({
|
|
|
98
157
|
controller,
|
|
99
158
|
focusInput,
|
|
100
159
|
messages,
|
|
160
|
+
persistDraft,
|
|
161
|
+
replaceLocalComposer,
|
|
162
|
+
setEditingMessage,
|
|
101
163
|
setFollowMessages,
|
|
102
164
|
setReplyTo,
|
|
103
165
|
setSelectedIdx,
|
|
104
166
|
useDefaultControllerChannel,
|
|
105
167
|
]);
|
|
106
168
|
|
|
169
|
+
const beginEditMessage = useCallback((index: number, options?: { deferFocus?: boolean }) => {
|
|
170
|
+
if (!canSend || index < 0 || index >= messages.length) return false;
|
|
171
|
+
const message = messages[index] ?? null;
|
|
172
|
+
if (!message || message.id !== latestEditableMessageId) return false;
|
|
173
|
+
clearReplyTarget();
|
|
174
|
+
setEditingMessage(message);
|
|
175
|
+
setSelectedIdx(index);
|
|
176
|
+
setFollowMessages(index === messages.length - 1);
|
|
177
|
+
replaceLocalComposer(message.content);
|
|
178
|
+
persistDraft(message.content);
|
|
179
|
+
if (options?.deferFocus) {
|
|
180
|
+
queueMicrotask(() => focusInput());
|
|
181
|
+
} else {
|
|
182
|
+
focusInput();
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}, [
|
|
186
|
+
canSend,
|
|
187
|
+
clearReplyTarget,
|
|
188
|
+
focusInput,
|
|
189
|
+
latestEditableMessageId,
|
|
190
|
+
messages,
|
|
191
|
+
persistDraft,
|
|
192
|
+
replaceLocalComposer,
|
|
193
|
+
setEditingMessage,
|
|
194
|
+
setFollowMessages,
|
|
195
|
+
setSelectedIdx,
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const beginEditLatestMessage = useCallback((options?: { deferFocus?: boolean }) => {
|
|
199
|
+
if (!latestEditableMessageId) return false;
|
|
200
|
+
const index = messages.findIndex((message) => message.id === latestEditableMessageId);
|
|
201
|
+
return beginEditMessage(index, options);
|
|
202
|
+
}, [beginEditMessage, latestEditableMessageId, messages]);
|
|
203
|
+
|
|
204
|
+
const cancelEditMessage = useCallback(() => {
|
|
205
|
+
if (!editingMessageRef.current) return;
|
|
206
|
+
setEditingMessage(null);
|
|
207
|
+
replaceLocalComposer("");
|
|
208
|
+
persistDraft("");
|
|
209
|
+
focusInput();
|
|
210
|
+
}, [focusInput, persistDraft, replaceLocalComposer, setEditingMessage]);
|
|
211
|
+
|
|
107
212
|
const returnToComposer = useCallback(() => {
|
|
108
213
|
setSelectedIdx(-1);
|
|
109
214
|
setFollowMessages(true);
|
|
@@ -113,22 +218,30 @@ export function useChatComposerRuntime({
|
|
|
113
218
|
}, [canSend, focusInput, setFollowMessages, setSelectedIdx]);
|
|
114
219
|
|
|
115
220
|
const clearLocalComposer = useCallback(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const textarea = inputRef.current;
|
|
119
|
-
if (textarea && textarea.editBuffer.getText() !== "") {
|
|
120
|
-
applyingExternalDraftRef.current = true;
|
|
121
|
-
try {
|
|
122
|
-
textarea.setText("");
|
|
123
|
-
} finally {
|
|
124
|
-
applyingExternalDraftRef.current = false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}, [applyingExternalDraftRef, inputRef, inputValueRef, updateComposerRows]);
|
|
221
|
+
replaceLocalComposer("");
|
|
222
|
+
}, [replaceLocalComposer]);
|
|
128
223
|
|
|
129
224
|
const sendMessage = useCallback(() => {
|
|
130
225
|
const content = inputValueRef.current.trim();
|
|
131
226
|
if (!content) return;
|
|
227
|
+
const editing = editingMessageRef.current;
|
|
228
|
+
if (editing) {
|
|
229
|
+
if (editSubmittingRef.current) return;
|
|
230
|
+
const sendChannelId = useDefaultControllerChannel ? channelId : channelIdRef.current;
|
|
231
|
+
if (editing.channelId !== sendChannelId) return;
|
|
232
|
+
editSubmittingRef.current = true;
|
|
233
|
+
void controller.editChannelMessage(sendChannelId, editing.id, content).then((accepted) => {
|
|
234
|
+
if (!accepted) return;
|
|
235
|
+
setEditingMessage(null);
|
|
236
|
+
clearLocalComposer();
|
|
237
|
+
persistDraft("");
|
|
238
|
+
setSelectedIdx(-1);
|
|
239
|
+
setFollowMessages(true);
|
|
240
|
+
}).finally(() => {
|
|
241
|
+
editSubmittingRef.current = false;
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
132
245
|
const composerCommand = parseChatComposerCommand(content);
|
|
133
246
|
if (composerCommand?.kind === "direct") {
|
|
134
247
|
void controller.openDirectChannel({ username: composerCommand.username }).then((channel) => {
|
|
@@ -154,9 +267,13 @@ export function useChatComposerRuntime({
|
|
|
154
267
|
}).catch(() => {});
|
|
155
268
|
return;
|
|
156
269
|
}
|
|
270
|
+
const sendChannelId = useDefaultControllerChannel ? channelId : channelIdRef.current;
|
|
271
|
+
const replyToId = replyToRef.current?.channelId === sendChannelId
|
|
272
|
+
? replyToRef.current.id
|
|
273
|
+
: undefined;
|
|
157
274
|
const accepted = useDefaultControllerChannel
|
|
158
|
-
? controller.send(content,
|
|
159
|
-
: controller.sendToChannel(
|
|
275
|
+
? controller.send(content, replyToId)
|
|
276
|
+
: controller.sendToChannel(sendChannelId, content, replyToId);
|
|
160
277
|
if (!accepted) return;
|
|
161
278
|
clearLocalComposer();
|
|
162
279
|
setSelectedIdx(-1);
|
|
@@ -168,7 +285,9 @@ export function useChatComposerRuntime({
|
|
|
168
285
|
controller,
|
|
169
286
|
inputValueRef,
|
|
170
287
|
onChannelChange,
|
|
288
|
+
persistDraft,
|
|
171
289
|
setDirectExpanded,
|
|
290
|
+
setEditingMessage,
|
|
172
291
|
setFollowMessages,
|
|
173
292
|
setSelectedIdx,
|
|
174
293
|
useDefaultControllerChannel,
|
|
@@ -178,16 +297,13 @@ export function useChatComposerRuntime({
|
|
|
178
297
|
if (applyingExternalDraftRef.current) return;
|
|
179
298
|
inputValueRef.current = draft;
|
|
180
299
|
updateComposerRows(draft);
|
|
181
|
-
|
|
182
|
-
controller.setDraft(draft);
|
|
183
|
-
} else {
|
|
184
|
-
controller.setChannelDraft(channelId, draft);
|
|
185
|
-
}
|
|
300
|
+
persistDraft(draft);
|
|
186
301
|
}, [
|
|
187
302
|
applyingExternalDraftRef,
|
|
188
303
|
channelId,
|
|
189
304
|
controller,
|
|
190
305
|
inputValueRef,
|
|
306
|
+
persistDraft,
|
|
191
307
|
updateComposerRows,
|
|
192
308
|
useDefaultControllerChannel,
|
|
193
309
|
]);
|
|
@@ -230,18 +346,33 @@ export function useChatComposerRuntime({
|
|
|
230
346
|
clearReplyTarget();
|
|
231
347
|
}, [canSend, clearReplyTarget, replyTo]);
|
|
232
348
|
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (canSend || !editingMessage) return;
|
|
351
|
+
cancelEditMessage();
|
|
352
|
+
}, [canSend, cancelEditMessage, editingMessage]);
|
|
353
|
+
|
|
233
354
|
const replyPreview = replyTo
|
|
234
355
|
? formatInlinePreview(
|
|
235
356
|
replyTo.content,
|
|
236
357
|
Math.max(contentWidth - ` replying to @${replyTo.user.username}: `.length - COMPOSER_ACTION_WIDTH - 1, 0),
|
|
237
358
|
)
|
|
238
359
|
: "";
|
|
239
|
-
const
|
|
360
|
+
const editingPreview = editingMessage
|
|
361
|
+
? formatInlinePreview(
|
|
362
|
+
editingMessage.content,
|
|
363
|
+
Math.max(contentWidth - " editing: ".length - COMPOSER_ACTION_WIDTH - 1, 0),
|
|
364
|
+
)
|
|
365
|
+
: "";
|
|
366
|
+
const inputPlaceholder = editingMessage ? "Edit message..." : replyTo ? `Reply to @${replyTo.user.username}...` : "Type a message...";
|
|
240
367
|
|
|
241
368
|
return {
|
|
369
|
+
beginEditLatestMessage,
|
|
370
|
+
beginEditMessage,
|
|
242
371
|
beginReplyTo,
|
|
372
|
+
cancelEditMessage,
|
|
243
373
|
clearReplyTarget,
|
|
244
374
|
commitLocalDraft,
|
|
375
|
+
editingPreview,
|
|
245
376
|
focusComposer,
|
|
246
377
|
inputPlaceholder,
|
|
247
378
|
replyPreview,
|
|
@@ -14,6 +14,7 @@ interface MutableRef<T> {
|
|
|
14
14
|
|
|
15
15
|
interface ChatComposerAreaProps {
|
|
16
16
|
canSend: boolean;
|
|
17
|
+
cancelEditMessage: () => void;
|
|
17
18
|
clearReplyTarget: () => void;
|
|
18
19
|
commitLocalDraft: (draft: string) => void;
|
|
19
20
|
composerHeight: number;
|
|
@@ -27,6 +28,8 @@ interface ChatComposerAreaProps {
|
|
|
27
28
|
inputRef: MutableRef<TextareaRenderable | null>;
|
|
28
29
|
inputValueRef: MutableRef<string>;
|
|
29
30
|
nativePaneChrome: boolean | undefined;
|
|
31
|
+
editingMessage: ChatMessage | null;
|
|
32
|
+
editingPreview: string;
|
|
30
33
|
replyPreview: string;
|
|
31
34
|
replyTo: ChatMessage | null;
|
|
32
35
|
sendMessage: () => void;
|
|
@@ -35,6 +38,7 @@ interface ChatComposerAreaProps {
|
|
|
35
38
|
|
|
36
39
|
export function ChatComposerArea({
|
|
37
40
|
canSend,
|
|
41
|
+
cancelEditMessage,
|
|
38
42
|
clearReplyTarget,
|
|
39
43
|
commitLocalDraft,
|
|
40
44
|
composerHeight,
|
|
@@ -48,6 +52,8 @@ export function ChatComposerArea({
|
|
|
48
52
|
inputRef,
|
|
49
53
|
inputValueRef,
|
|
50
54
|
nativePaneChrome,
|
|
55
|
+
editingMessage,
|
|
56
|
+
editingPreview,
|
|
51
57
|
replyPreview,
|
|
52
58
|
replyTo,
|
|
53
59
|
sendMessage,
|
|
@@ -89,7 +95,20 @@ export function ChatComposerArea({
|
|
|
89
95
|
/>
|
|
90
96
|
)}
|
|
91
97
|
|
|
92
|
-
{
|
|
98
|
+
{editingMessage && (
|
|
99
|
+
<Box height={1} width={contentWidth} flexDirection="row">
|
|
100
|
+
<Text fg={colors.textMuted}> editing </Text>
|
|
101
|
+
<Text fg={colors.textDim}>{editingPreview ? `: ${editingPreview}` : ""}</Text>
|
|
102
|
+
<Box flexGrow={1} />
|
|
103
|
+
<ChatActionChip
|
|
104
|
+
label="Cancel"
|
|
105
|
+
width={COMPOSER_ACTION_WIDTH}
|
|
106
|
+
onPress={cancelEditMessage}
|
|
107
|
+
/>
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{!editingMessage && replyTo && (
|
|
93
112
|
<Box height={1} width={contentWidth} flexDirection="row">
|
|
94
113
|
<Text fg={colors.textMuted}> replying to </Text>
|
|
95
114
|
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{`@${replyTo.user.username}`}</Text>
|
|
@@ -135,14 +154,16 @@ export function ChatComposerArea({
|
|
|
135
154
|
export function getChatComposerAreaHeight({
|
|
136
155
|
canSend,
|
|
137
156
|
composerHeight,
|
|
157
|
+
editingMessage,
|
|
138
158
|
nativePaneChrome,
|
|
139
159
|
replyTo,
|
|
140
160
|
}: {
|
|
141
161
|
canSend: boolean;
|
|
142
162
|
composerHeight: number;
|
|
163
|
+
editingMessage: ChatMessage | null;
|
|
143
164
|
nativePaneChrome: boolean | undefined;
|
|
144
165
|
replyTo: ChatMessage | null;
|
|
145
166
|
}) {
|
|
146
167
|
if (!canSend) return 2;
|
|
147
|
-
return getMessageComposerBlockHeight({ height: composerHeight, nativePaneChrome }) + (replyTo ? 1 : 0);
|
|
168
|
+
return getMessageComposerBlockHeight({ height: composerHeight, nativePaneChrome }) + (editingMessage || replyTo ? 1 : 0);
|
|
148
169
|
}
|