gloomberb 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -52
- package/package.json +1 -1
- 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/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/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/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 +11 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- 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 +20 -6
- package/src/components/ui/ui.test.tsx +30 -5
- 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 +6 -0
- 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 +61 -0
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +20 -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 +1 -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/index.tsx +6 -0
- 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/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/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/world-indices/index.tsx +9 -6
- 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/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/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/test-support/plugin-runtime.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -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
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../layout";
|
|
11
11
|
import {
|
|
12
12
|
DEFAULT_CHAT_CHANNEL_ID,
|
|
13
|
+
formatChatPaneTitle,
|
|
13
14
|
normalizeChannelId,
|
|
14
15
|
} from "../channels";
|
|
15
16
|
import { ChatComposerArea } from "./composer";
|
|
@@ -30,6 +31,11 @@ import {
|
|
|
30
31
|
import { buildChatUserByUsername } from "./user-map";
|
|
31
32
|
import { useChatComposerRuntime } from "./composer-runtime";
|
|
32
33
|
import { useChatMessageSelection } from "./selection-runtime";
|
|
34
|
+
import type { ChatMessage } from "../../../../api-client";
|
|
35
|
+
import {
|
|
36
|
+
CHAT_MESSAGE_EDIT_WINDOW_MS,
|
|
37
|
+
findLatestEditableChatMessage,
|
|
38
|
+
} from "../edit-window";
|
|
33
39
|
|
|
34
40
|
interface ChatContentProps {
|
|
35
41
|
width: number;
|
|
@@ -37,6 +43,7 @@ interface ChatContentProps {
|
|
|
37
43
|
focused: boolean;
|
|
38
44
|
channelId?: string;
|
|
39
45
|
onChannelChange?: (channelId: string) => void;
|
|
46
|
+
onChannelTitleChange?: (title: string) => void;
|
|
40
47
|
controller?: ChatContentController;
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -46,6 +53,7 @@ export function ChatContent({
|
|
|
46
53
|
focused,
|
|
47
54
|
channelId: rawChannelId,
|
|
48
55
|
onChannelChange,
|
|
56
|
+
onChannelTitleChange,
|
|
49
57
|
controller = chatController,
|
|
50
58
|
}: ChatContentProps) {
|
|
51
59
|
const dispatch = useAppDispatch();
|
|
@@ -57,12 +65,14 @@ export function ChatContent({
|
|
|
57
65
|
const [inputFocused, setInputFocused] = useState(false);
|
|
58
66
|
const [selectedIdx, setSelectedIdx] = useState(-1);
|
|
59
67
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
68
|
+
const [editingMessage, setEditingMessage] = useState<ChatMessage | null>(null);
|
|
60
69
|
const [followMessages, setFollowMessages] = useState(true);
|
|
61
70
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
62
71
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
63
72
|
const messageElementsRef = useRef(new Map<string, unknown>());
|
|
64
73
|
const applyingExternalDraftRef = useRef(false);
|
|
65
74
|
const prependAnchorRef = useRef<ChatPrependAnchor | null>(null);
|
|
75
|
+
const previousEditingChannelIdRef = useRef(channelId);
|
|
66
76
|
const useDefaultControllerChannel = channelId === DEFAULT_CHAT_CHANNEL_ID && !onChannelChange;
|
|
67
77
|
const initialWidthMetrics = resolveChatContentWidthMetrics({
|
|
68
78
|
width,
|
|
@@ -122,12 +132,36 @@ export function ChatContent({
|
|
|
122
132
|
const selectionActive = selectedIdx >= 0 && selectedIdx < messages.length;
|
|
123
133
|
const stickyTranscript = followMessages && !selectionActive;
|
|
124
134
|
const latestMessageId = messages[messages.length - 1]?.id ?? null;
|
|
135
|
+
const [editWindowNowMs, setEditWindowNowMs] = useState(() => Date.now());
|
|
136
|
+
const latestOwnMessage = useMemo(() => {
|
|
137
|
+
if (!user?.id) return null;
|
|
138
|
+
return [...messages]
|
|
139
|
+
.reverse()
|
|
140
|
+
.find((message) => message.user.id === user.id && !message.clientStatus) ?? null;
|
|
141
|
+
}, [messages, user?.id]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!latestOwnMessage) return;
|
|
145
|
+
const createdMs = Date.parse(latestOwnMessage.createdAt);
|
|
146
|
+
if (!Number.isFinite(createdMs)) return;
|
|
147
|
+
const expiresInMs = createdMs + CHAT_MESSAGE_EDIT_WINDOW_MS - Date.now();
|
|
148
|
+
if (expiresInMs <= 0) return;
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
setEditWindowNowMs(Date.now());
|
|
151
|
+
}, Math.min(expiresInMs + 250, 60_000));
|
|
152
|
+
return () => clearTimeout(timer);
|
|
153
|
+
}, [editWindowNowMs, latestOwnMessage]);
|
|
154
|
+
|
|
155
|
+
const latestEditableMessageId = useMemo(() => {
|
|
156
|
+
return findLatestEditableChatMessage(messages, user?.id, editWindowNowMs)?.id ?? null;
|
|
157
|
+
}, [editWindowNowMs, messages, user?.id]);
|
|
125
158
|
const {
|
|
126
159
|
composerHeight,
|
|
127
160
|
messageAreaHeight,
|
|
128
161
|
} = resolveChatContentHeightMetrics({
|
|
129
162
|
canSend,
|
|
130
163
|
composerRows,
|
|
164
|
+
editingMessage,
|
|
131
165
|
height,
|
|
132
166
|
nativePaneChrome,
|
|
133
167
|
replyTo,
|
|
@@ -140,6 +174,8 @@ export function ChatContent({
|
|
|
140
174
|
const messageContents = useMemo(() => messages.map((message) => message.content), [messages]);
|
|
141
175
|
const { catalog, openTicker } = useInlineTickers(messageContents);
|
|
142
176
|
const userByUsername = useMemo(() => buildChatUserByUsername(channels, messages), [channels, messages]);
|
|
177
|
+
const activeChannel = useMemo(() => channels.find((channel) => channel.id === channelId), [channelId, channels]);
|
|
178
|
+
const activeChannelTitle = useMemo(() => formatChatPaneTitle(activeChannel, channelId), [activeChannel, channelId]);
|
|
143
179
|
const {
|
|
144
180
|
cancelProfilePopoverClose,
|
|
145
181
|
closeProfilePopover,
|
|
@@ -153,6 +189,16 @@ export function ChatContent({
|
|
|
153
189
|
dispatch({ type: "SET_INPUT_CAPTURED", captured: false });
|
|
154
190
|
}, [dispatch]);
|
|
155
191
|
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
onChannelTitleChange?.(activeChannelTitle);
|
|
194
|
+
}, [activeChannelTitle, onChannelTitleChange]);
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (previousEditingChannelIdRef.current === channelId) return;
|
|
198
|
+
previousEditingChannelIdRef.current = channelId;
|
|
199
|
+
setEditingMessage(null);
|
|
200
|
+
}, [channelId]);
|
|
201
|
+
|
|
156
202
|
const {
|
|
157
203
|
moveMessageSelection,
|
|
158
204
|
resetTranscriptSelection,
|
|
@@ -200,9 +246,13 @@ export function ChatContent({
|
|
|
200
246
|
}, [dispatch, setSidebarFocused]);
|
|
201
247
|
|
|
202
248
|
const {
|
|
249
|
+
beginEditLatestMessage,
|
|
250
|
+
beginEditMessage,
|
|
203
251
|
beginReplyTo,
|
|
252
|
+
cancelEditMessage,
|
|
204
253
|
clearReplyTarget,
|
|
205
254
|
commitLocalDraft,
|
|
255
|
+
editingPreview,
|
|
206
256
|
focusComposer,
|
|
207
257
|
inputPlaceholder,
|
|
208
258
|
replyPreview,
|
|
@@ -223,7 +273,10 @@ export function ChatContent({
|
|
|
223
273
|
inputValueRef,
|
|
224
274
|
messages,
|
|
225
275
|
onChannelChange,
|
|
276
|
+
editingMessage,
|
|
277
|
+
latestEditableMessageId,
|
|
226
278
|
replyTo,
|
|
279
|
+
setEditingMessage,
|
|
227
280
|
setDirectExpanded,
|
|
228
281
|
setFollowMessages,
|
|
229
282
|
setReplyTo,
|
|
@@ -263,9 +316,11 @@ export function ChatContent({
|
|
|
263
316
|
});
|
|
264
317
|
|
|
265
318
|
useChatContentShortcuts({
|
|
319
|
+
beginEditLatestMessage,
|
|
266
320
|
beginReplyTo,
|
|
267
321
|
blurInput,
|
|
268
322
|
canSend,
|
|
323
|
+
cancelEditMessage,
|
|
269
324
|
clearReplyTarget,
|
|
270
325
|
cycleChannel,
|
|
271
326
|
focusChannelSidebar,
|
|
@@ -280,6 +335,7 @@ export function ChatContent({
|
|
|
280
335
|
moveMessageSelection,
|
|
281
336
|
moveSidebarChannelSelection,
|
|
282
337
|
nativePaneChrome,
|
|
338
|
+
editingMessage,
|
|
283
339
|
replyTo,
|
|
284
340
|
requestOlderMessages,
|
|
285
341
|
requestOlderMessagesIfNeeded,
|
|
@@ -347,6 +403,7 @@ export function ChatContent({
|
|
|
347
403
|
|
|
348
404
|
<ChatTranscript
|
|
349
405
|
beginReplyTo={beginReplyTo}
|
|
406
|
+
beginEditMessage={beginEditMessage}
|
|
350
407
|
canSend={canSend}
|
|
351
408
|
catalog={catalog}
|
|
352
409
|
cancelProfilePopoverClose={cancelProfilePopoverClose}
|
|
@@ -361,6 +418,7 @@ export function ChatContent({
|
|
|
361
418
|
messageBodyWidth={messageBodyWidth}
|
|
362
419
|
messages={messages}
|
|
363
420
|
nativePaneChrome={nativePaneChrome}
|
|
421
|
+
latestEditableMessageId={latestEditableMessageId}
|
|
364
422
|
openDirectMessage={openDirectMessage}
|
|
365
423
|
openTicker={openTicker}
|
|
366
424
|
profilePopoverUser={profilePopoverUser}
|
|
@@ -383,6 +441,7 @@ export function ChatContent({
|
|
|
383
441
|
|
|
384
442
|
<ChatComposerArea
|
|
385
443
|
canSend={canSend}
|
|
444
|
+
cancelEditMessage={cancelEditMessage}
|
|
386
445
|
clearReplyTarget={clearReplyTarget}
|
|
387
446
|
commitLocalDraft={commitLocalDraft}
|
|
388
447
|
composerHeight={composerHeight}
|
|
@@ -396,6 +455,8 @@ export function ChatContent({
|
|
|
396
455
|
inputRef={inputRef}
|
|
397
456
|
inputValueRef={inputValueRef}
|
|
398
457
|
nativePaneChrome={nativePaneChrome}
|
|
458
|
+
editingMessage={editingMessage}
|
|
459
|
+
editingPreview={editingPreview}
|
|
399
460
|
replyPreview={replyPreview}
|
|
400
461
|
replyTo={replyTo}
|
|
401
462
|
sendMessage={sendMessage}
|
|
@@ -42,12 +42,14 @@ export function resolveChatContentWidthMetrics({
|
|
|
42
42
|
export function resolveChatContentHeightMetrics({
|
|
43
43
|
canSend,
|
|
44
44
|
composerRows,
|
|
45
|
+
editingMessage,
|
|
45
46
|
height,
|
|
46
47
|
nativePaneChrome,
|
|
47
48
|
replyTo,
|
|
48
49
|
}: {
|
|
49
50
|
canSend: boolean;
|
|
50
51
|
composerRows: number;
|
|
52
|
+
editingMessage: ChatMessage | null;
|
|
51
53
|
height: number;
|
|
52
54
|
nativePaneChrome: boolean | undefined;
|
|
53
55
|
replyTo: ChatMessage | null;
|
|
@@ -57,7 +59,7 @@ export function resolveChatContentHeightMetrics({
|
|
|
57
59
|
? Math.min(CHAT_COMPOSER_MAX_ROWS + 1, Math.max(2, composerRows + 1))
|
|
58
60
|
: composerRows
|
|
59
61
|
: 0;
|
|
60
|
-
const inputAreaHeight = getChatComposerAreaHeight({ canSend, composerHeight, nativePaneChrome, replyTo });
|
|
62
|
+
const inputAreaHeight = getChatComposerAreaHeight({ canSend, composerHeight, editingMessage, nativePaneChrome, replyTo });
|
|
61
63
|
const topSeparatorHeight = nativePaneChrome ? 0 : 1;
|
|
62
64
|
const footerSeparatorHeight = !nativePaneChrome && !canSend ? 1 : 0;
|
|
63
65
|
const messageAreaHeight = Math.max(1, height - topSeparatorHeight - footerSeparatorHeight - inputAreaHeight);
|
|
@@ -6,9 +6,11 @@ import { isPlainKey } from "../../../../utils/keyboard";
|
|
|
6
6
|
import { scrollToBottom } from "../layout";
|
|
7
7
|
|
|
8
8
|
export function useChatContentShortcuts({
|
|
9
|
+
beginEditLatestMessage,
|
|
9
10
|
beginReplyTo,
|
|
10
11
|
blurInput,
|
|
11
12
|
canSend,
|
|
13
|
+
cancelEditMessage,
|
|
12
14
|
clearReplyTarget,
|
|
13
15
|
cycleChannel,
|
|
14
16
|
focusChannelSidebar,
|
|
@@ -23,6 +25,7 @@ export function useChatContentShortcuts({
|
|
|
23
25
|
moveMessageSelection,
|
|
24
26
|
moveSidebarChannelSelection,
|
|
25
27
|
nativePaneChrome,
|
|
28
|
+
editingMessage,
|
|
26
29
|
replyTo,
|
|
27
30
|
requestOlderMessages,
|
|
28
31
|
requestOlderMessagesIfNeeded,
|
|
@@ -35,9 +38,11 @@ export function useChatContentShortcuts({
|
|
|
35
38
|
showChannelSidebar,
|
|
36
39
|
sidebarFocusedRef,
|
|
37
40
|
}: {
|
|
41
|
+
beginEditLatestMessage: (options?: { deferFocus?: boolean }) => boolean;
|
|
38
42
|
beginReplyTo: (index: number, options?: { deferFocus?: boolean }) => void;
|
|
39
43
|
blurInput: () => void;
|
|
40
44
|
canSend: boolean;
|
|
45
|
+
cancelEditMessage: () => void;
|
|
41
46
|
clearReplyTarget: () => void;
|
|
42
47
|
cycleChannel: (direction: 1 | -1) => boolean;
|
|
43
48
|
focusChannelSidebar: () => boolean;
|
|
@@ -52,6 +57,7 @@ export function useChatContentShortcuts({
|
|
|
52
57
|
moveMessageSelection: (direction: "up" | "down") => boolean;
|
|
53
58
|
moveSidebarChannelSelection: (direction: "up" | "down") => boolean;
|
|
54
59
|
nativePaneChrome?: boolean;
|
|
60
|
+
editingMessage: ChatMessage | null;
|
|
55
61
|
replyTo: ChatMessage | null;
|
|
56
62
|
requestOlderMessages: () => void;
|
|
57
63
|
requestOlderMessagesIfNeeded: () => void;
|
|
@@ -107,7 +113,9 @@ export function useChatContentShortcuts({
|
|
|
107
113
|
if (event.name === "escape") {
|
|
108
114
|
event.preventDefault?.();
|
|
109
115
|
event.stopPropagation?.();
|
|
110
|
-
if (
|
|
116
|
+
if (editingMessage) {
|
|
117
|
+
cancelEditMessage();
|
|
118
|
+
} else if (replyTo) {
|
|
111
119
|
clearReplyTarget();
|
|
112
120
|
} else {
|
|
113
121
|
blurInput();
|
|
@@ -116,6 +124,17 @@ export function useChatContentShortcuts({
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
const verticalDirection = event.name === "up" || event.name === "down" ? event.name : null;
|
|
127
|
+
if (
|
|
128
|
+
verticalDirection === "up"
|
|
129
|
+
&& canSend
|
|
130
|
+
&& isPlainKey(event, "up")
|
|
131
|
+
&& inputValueRef.current.trim().length === 0
|
|
132
|
+
&& beginEditLatestMessage({ deferFocus: true })
|
|
133
|
+
) {
|
|
134
|
+
event.preventDefault?.();
|
|
135
|
+
event.stopPropagation?.();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
119
138
|
if (verticalDirection && isPlainKey(event, "up", "down") && shouldLeaveComposerForSelection(verticalDirection)) {
|
|
120
139
|
const moved = moveMessageSelection(verticalDirection);
|
|
121
140
|
if (moved) {
|
|
@@ -13,6 +13,7 @@ interface MutableRef<T> {
|
|
|
13
13
|
|
|
14
14
|
interface ChatTranscriptProps {
|
|
15
15
|
beginReplyTo: (index: number, options?: { deferFocus?: boolean }) => void;
|
|
16
|
+
beginEditMessage: (index: number, options?: { deferFocus?: boolean }) => boolean;
|
|
16
17
|
canSend: boolean;
|
|
17
18
|
catalog: Record<string, InlineTickerCatalogEntry>;
|
|
18
19
|
cancelProfilePopoverClose: () => void;
|
|
@@ -27,6 +28,7 @@ interface ChatTranscriptProps {
|
|
|
27
28
|
messageBodyWidth: number;
|
|
28
29
|
messages: ChatMessage[];
|
|
29
30
|
nativePaneChrome: boolean | undefined;
|
|
31
|
+
latestEditableMessageId: string | null;
|
|
30
32
|
openDirectMessage: (target: ChatUserSummary) => void;
|
|
31
33
|
openTicker: (symbol: string) => void;
|
|
32
34
|
profilePopoverUser: ChatUserSummary | null;
|
|
@@ -43,6 +45,7 @@ interface ChatTranscriptProps {
|
|
|
43
45
|
|
|
44
46
|
export function ChatTranscript({
|
|
45
47
|
beginReplyTo,
|
|
48
|
+
beginEditMessage,
|
|
46
49
|
canSend,
|
|
47
50
|
catalog,
|
|
48
51
|
cancelProfilePopoverClose,
|
|
@@ -57,6 +60,7 @@ export function ChatTranscript({
|
|
|
57
60
|
messageBodyWidth,
|
|
58
61
|
messages,
|
|
59
62
|
nativePaneChrome,
|
|
63
|
+
latestEditableMessageId,
|
|
60
64
|
openDirectMessage,
|
|
61
65
|
openTicker,
|
|
62
66
|
profilePopoverUser,
|
|
@@ -113,7 +117,9 @@ export function ChatTranscript({
|
|
|
113
117
|
onUserHover={showProfilePopover}
|
|
114
118
|
onUserHoverEnd={scheduleProfilePopoverClose}
|
|
115
119
|
beginReplyTo={beginReplyTo}
|
|
120
|
+
beginEditMessage={beginEditMessage}
|
|
116
121
|
jumpToMessage={jumpToMessage}
|
|
122
|
+
latestEditableMessageId={latestEditableMessageId}
|
|
117
123
|
registerMessageElement={registerMessageElement}
|
|
118
124
|
/>
|
|
119
125
|
) : (
|
|
@@ -133,7 +139,9 @@ export function ChatTranscript({
|
|
|
133
139
|
onUserHover={showProfilePopover}
|
|
134
140
|
onUserHoverEnd={scheduleProfilePopoverClose}
|
|
135
141
|
beginReplyTo={beginReplyTo}
|
|
142
|
+
beginEditMessage={beginEditMessage}
|
|
136
143
|
jumpToMessage={jumpToMessage}
|
|
144
|
+
latestEditableMessageId={latestEditableMessageId}
|
|
137
145
|
setHoveredIdx={setHoveredIdx}
|
|
138
146
|
/>
|
|
139
147
|
)
|