gloomberb 0.8.1 → 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
|
@@ -22,14 +22,20 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
22
22
|
onUserHover,
|
|
23
23
|
onUserHoverEnd,
|
|
24
24
|
beginReplyTo,
|
|
25
|
+
beginEditMessage,
|
|
25
26
|
jumpToMessage,
|
|
27
|
+
latestEditableMessageId,
|
|
26
28
|
registerMessageElement,
|
|
27
29
|
}: ChatMessageBaseProps & {
|
|
28
30
|
registerMessageElement: (messageId: string, node: unknown | null) => void;
|
|
29
31
|
}) {
|
|
30
32
|
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
33
|
+
const canEditMessage = msg.id === latestEditableMessageId;
|
|
31
34
|
const showInlineReplyAction = !state.grouped && canSend;
|
|
35
|
+
const showInlineEditAction = !state.grouped && canSend && canEditMessage;
|
|
32
36
|
const showGroupedReplyAction = state.grouped && canSend;
|
|
37
|
+
const showGroupedEditAction = state.grouped && canSend && canEditMessage;
|
|
38
|
+
const groupedActionWidth = MESSAGE_ACTION_WIDTH * Number(showGroupedReplyAction) + MESSAGE_ACTION_WIDTH * Number(showGroupedEditAction);
|
|
33
39
|
const rowProps = {
|
|
34
40
|
width: "100%",
|
|
35
41
|
paddingRight: DESKTOP_MESSAGE_RIGHT_PADDING,
|
|
@@ -98,21 +104,37 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
98
104
|
{msg.user.username ?? "anon"}
|
|
99
105
|
</Text>
|
|
100
106
|
<Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
|
|
101
|
-
{showInlineReplyAction && (
|
|
107
|
+
{(showInlineReplyAction || showInlineEditAction) && (
|
|
102
108
|
<>
|
|
103
109
|
<Text fg={state.headerStatusColor}> </Text>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
{showInlineReplyAction && (
|
|
111
|
+
<Box
|
|
112
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
113
|
+
height={1}
|
|
114
|
+
data-gloom-role="chat-message-reply-action"
|
|
115
|
+
>
|
|
116
|
+
<ChatActionChip
|
|
117
|
+
label="Reply"
|
|
118
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
119
|
+
emphasized={state.isSelected}
|
|
120
|
+
onPress={() => beginReplyTo(index)}
|
|
121
|
+
/>
|
|
122
|
+
</Box>
|
|
123
|
+
)}
|
|
124
|
+
{showInlineEditAction && (
|
|
125
|
+
<Box
|
|
111
126
|
width={MESSAGE_ACTION_WIDTH}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
height={1}
|
|
128
|
+
data-gloom-role="chat-message-reply-action"
|
|
129
|
+
>
|
|
130
|
+
<ChatActionChip
|
|
131
|
+
label="Edit"
|
|
132
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
133
|
+
emphasized={state.isSelected}
|
|
134
|
+
onPress={() => beginEditMessage(index)}
|
|
135
|
+
/>
|
|
136
|
+
</Box>
|
|
137
|
+
)}
|
|
116
138
|
</>
|
|
117
139
|
)}
|
|
118
140
|
</Box>
|
|
@@ -135,21 +157,32 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
135
157
|
onUserHoverEnd={onUserHoverEnd}
|
|
136
158
|
/>
|
|
137
159
|
</Box>
|
|
138
|
-
{showGroupedReplyAction && (
|
|
160
|
+
{(showGroupedReplyAction || showGroupedEditAction) && (
|
|
139
161
|
<Box
|
|
140
162
|
position="absolute"
|
|
141
163
|
top={0}
|
|
142
164
|
right={0}
|
|
143
|
-
width={
|
|
165
|
+
width={groupedActionWidth}
|
|
144
166
|
height={1}
|
|
167
|
+
flexDirection="row"
|
|
145
168
|
data-gloom-role="chat-message-reply-action"
|
|
146
169
|
>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
{showGroupedReplyAction && (
|
|
171
|
+
<ChatActionChip
|
|
172
|
+
label="Reply"
|
|
173
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
174
|
+
emphasized={state.isSelected}
|
|
175
|
+
onPress={() => beginReplyTo(index)}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
{showGroupedEditAction && (
|
|
179
|
+
<ChatActionChip
|
|
180
|
+
label="Edit"
|
|
181
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
182
|
+
emphasized={state.isSelected}
|
|
183
|
+
onPress={() => beginEditMessage(index)}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
153
186
|
</Box>
|
|
154
187
|
)}
|
|
155
188
|
</Box>
|
|
@@ -6,10 +6,12 @@ import { TickerBadge } from "../../../../components/ticker/badge";
|
|
|
6
6
|
import type { InlineTickerCatalogEntry } from "../../../../state/hooks/inline-tickers";
|
|
7
7
|
import { blendHex, colors } from "../../../../theme/colors";
|
|
8
8
|
import type { ChatUserSummary } from "../../../../api-client";
|
|
9
|
-
import { tokenizeInlineContent } from "../../../../utils/inline-content-tokenizer";
|
|
9
|
+
import { tokenizeInlineContent, type InlineContentToken } from "../../../../utils/inline-content-tokenizer";
|
|
10
10
|
|
|
11
11
|
export function ResponsiveTickerBadgeText({
|
|
12
|
-
text,
|
|
12
|
+
text = "",
|
|
13
|
+
tokens: providedTokens,
|
|
14
|
+
prewrapped = false,
|
|
13
15
|
catalog,
|
|
14
16
|
textColor,
|
|
15
17
|
openTicker,
|
|
@@ -17,7 +19,9 @@ export function ResponsiveTickerBadgeText({
|
|
|
17
19
|
onUserHover,
|
|
18
20
|
onUserHoverEnd,
|
|
19
21
|
}: {
|
|
20
|
-
text
|
|
22
|
+
text?: string;
|
|
23
|
+
tokens?: readonly InlineContentToken[];
|
|
24
|
+
prewrapped?: boolean;
|
|
21
25
|
catalog: Record<string, InlineTickerCatalogEntry>;
|
|
22
26
|
textColor: string;
|
|
23
27
|
openTicker: (symbol: string) => void;
|
|
@@ -26,19 +30,21 @@ export function ResponsiveTickerBadgeText({
|
|
|
26
30
|
onUserHoverEnd?: () => void;
|
|
27
31
|
}) {
|
|
28
32
|
const [hoveredSymbol, setHoveredSymbol] = useState<string | null>(null);
|
|
29
|
-
const tokens = useMemo(() => tokenizeInlineContent(text), [text]);
|
|
33
|
+
const tokens = useMemo(() => providedTokens ?? tokenizeInlineContent(text), [providedTokens, text]);
|
|
30
34
|
const renderTextToken = (value: string, tokenIndex: number) => {
|
|
31
35
|
if (!value) return null;
|
|
32
36
|
return (
|
|
33
37
|
<Text
|
|
34
38
|
key={`text:${tokenIndex}`}
|
|
35
39
|
fg={textColor}
|
|
36
|
-
wrapText
|
|
37
|
-
style={
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
wrapText={!prewrapped}
|
|
41
|
+
style={prewrapped
|
|
42
|
+
? undefined
|
|
43
|
+
: {
|
|
44
|
+
minWidth: 0,
|
|
45
|
+
whiteSpace: "pre-wrap",
|
|
46
|
+
overflowWrap: "anywhere",
|
|
47
|
+
}}
|
|
42
48
|
>
|
|
43
49
|
{value}
|
|
44
50
|
</Text>
|
|
@@ -41,7 +41,11 @@ export function getChatMessageRenderState({
|
|
|
41
41
|
const isSending = msg.clientStatus === "sending";
|
|
42
42
|
const hasFailed = msg.clientStatus === "failed";
|
|
43
43
|
const selectedTextColor = hasFailed ? colors.negative : colors.selectedText;
|
|
44
|
-
const headerStatus = isSending
|
|
44
|
+
const headerStatus = isSending
|
|
45
|
+
? "sending..."
|
|
46
|
+
: hasFailed
|
|
47
|
+
? "failed"
|
|
48
|
+
: `${formatTimeAgo(msg.createdAt)}${msg.editedAt ? " edited" : ""}`;
|
|
45
49
|
|
|
46
50
|
return {
|
|
47
51
|
isSelected,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Text } from "../../../../ui";
|
|
2
|
-
import { MESSAGE_ACTION_WIDTH, formatInlinePreview,
|
|
2
|
+
import { MESSAGE_ACTION_WIDTH, formatInlinePreview, getMessageBodyTokenLines } from "../layout";
|
|
3
3
|
import { ChatActionChip } from "./action-chip";
|
|
4
4
|
import { ResponsiveTickerBadgeText } from "./inline-tokens";
|
|
5
5
|
import { getChatMessageRenderState } from "./render-state";
|
|
@@ -26,13 +26,19 @@ export function TerminalChatMessage({
|
|
|
26
26
|
onUserHover,
|
|
27
27
|
onUserHoverEnd,
|
|
28
28
|
beginReplyTo,
|
|
29
|
+
beginEditMessage,
|
|
29
30
|
jumpToMessage,
|
|
31
|
+
latestEditableMessageId,
|
|
30
32
|
setHoveredIdx,
|
|
31
33
|
}: TerminalChatMessageProps) {
|
|
32
34
|
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
33
|
-
const bodyLines =
|
|
35
|
+
const bodyLines = getMessageBodyTokenLines(msg.content, messageBodyWidth, catalog);
|
|
36
|
+
const canEditMessage = msg.id === latestEditableMessageId;
|
|
34
37
|
const showInlineReplyAction = !state.grouped && state.showReplyAction;
|
|
38
|
+
const showInlineEditAction = !state.grouped && state.showReplyAction && canEditMessage;
|
|
35
39
|
const showGroupedReplyAction = state.grouped && state.showReplyAction;
|
|
40
|
+
const showGroupedEditAction = state.grouped && state.showReplyAction && canEditMessage;
|
|
41
|
+
const groupedActionWidth = MESSAGE_ACTION_WIDTH * Number(showGroupedReplyAction) + MESSAGE_ACTION_WIDTH * Number(showGroupedEditAction);
|
|
36
42
|
const setHovered = () => setHoveredIdx((current) => (current === index ? current : index));
|
|
37
43
|
const clearHovered = () => setHoveredIdx((current) => (current === index ? null : current));
|
|
38
44
|
const messageRowProps = {
|
|
@@ -80,17 +86,29 @@ export function TerminalChatMessage({
|
|
|
80
86
|
{msg.user.username ?? "anon"}
|
|
81
87
|
</Text>
|
|
82
88
|
<Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
|
|
83
|
-
{showInlineReplyAction && (
|
|
89
|
+
{(showInlineReplyAction || showInlineEditAction) && (
|
|
84
90
|
<>
|
|
85
91
|
<Text fg={state.headerStatusColor}> </Text>
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
{showInlineReplyAction && (
|
|
93
|
+
<Box width={MESSAGE_ACTION_WIDTH} height={1}>
|
|
94
|
+
<ChatActionChip
|
|
95
|
+
label="Reply"
|
|
96
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
97
|
+
emphasized={state.isSelected}
|
|
98
|
+
onPress={() => beginReplyTo(index)}
|
|
99
|
+
/>
|
|
100
|
+
</Box>
|
|
101
|
+
)}
|
|
102
|
+
{showInlineEditAction && (
|
|
103
|
+
<Box width={MESSAGE_ACTION_WIDTH} height={1}>
|
|
104
|
+
<ChatActionChip
|
|
105
|
+
label="Edit"
|
|
106
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
107
|
+
emphasized={state.isSelected}
|
|
108
|
+
onPress={() => beginEditMessage(index)}
|
|
109
|
+
/>
|
|
110
|
+
</Box>
|
|
111
|
+
)}
|
|
94
112
|
</>
|
|
95
113
|
)}
|
|
96
114
|
</Box>
|
|
@@ -106,7 +124,8 @@ export function TerminalChatMessage({
|
|
|
106
124
|
>
|
|
107
125
|
<Box width={messageBodyWidth} height={1}>
|
|
108
126
|
<ResponsiveTickerBadgeText
|
|
109
|
-
|
|
127
|
+
tokens={line}
|
|
128
|
+
prewrapped
|
|
110
129
|
catalog={catalog}
|
|
111
130
|
textColor={state.bodyColor}
|
|
112
131
|
openTicker={openTicker}
|
|
@@ -115,14 +134,24 @@ export function TerminalChatMessage({
|
|
|
115
134
|
onUserHoverEnd={onUserHoverEnd}
|
|
116
135
|
/>
|
|
117
136
|
</Box>
|
|
118
|
-
{lineIndex === 0 && showGroupedReplyAction && (
|
|
119
|
-
<Box position="absolute" top={0} right={0} width={
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
{lineIndex === 0 && (showGroupedReplyAction || showGroupedEditAction) && (
|
|
138
|
+
<Box position="absolute" top={0} right={0} width={groupedActionWidth} height={1} flexDirection="row">
|
|
139
|
+
{showGroupedReplyAction && (
|
|
140
|
+
<ChatActionChip
|
|
141
|
+
label="Reply"
|
|
142
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
143
|
+
emphasized={state.isSelected}
|
|
144
|
+
onPress={() => beginReplyTo(index)}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
{showGroupedEditAction && (
|
|
148
|
+
<ChatActionChip
|
|
149
|
+
label="Edit"
|
|
150
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
151
|
+
emphasized={state.isSelected}
|
|
152
|
+
onPress={() => beginEditMessage(index)}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
126
155
|
</Box>
|
|
127
156
|
)}
|
|
128
157
|
</Box>
|
|
@@ -14,5 +14,7 @@ export interface ChatMessageBaseProps {
|
|
|
14
14
|
onUserHover: (user: ChatUserSummary) => void;
|
|
15
15
|
onUserHoverEnd: () => void;
|
|
16
16
|
beginReplyTo: (index: number, options?: { deferFocus?: boolean }) => void;
|
|
17
|
+
beginEditMessage: (index: number, options?: { deferFocus?: boolean }) => boolean;
|
|
17
18
|
jumpToMessage: (messageId: string) => void;
|
|
19
|
+
latestEditableMessageId: string | null;
|
|
18
20
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { updatePaneInstance } from "../../../pane-settings";
|
|
3
3
|
import {
|
|
4
4
|
syncConfigActiveLayoutState,
|
|
5
5
|
useAppDispatch,
|
|
@@ -12,6 +12,7 @@ import { scheduleConfigSave } from "../../../state/config-save-scheduler";
|
|
|
12
12
|
import type { PaneProps } from "../../../types/plugin";
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_CHAT_CHANNEL_ID,
|
|
15
|
+
formatChatPaneTitle,
|
|
15
16
|
LAST_VISITED_CHAT_CHANNEL_KEY,
|
|
16
17
|
normalizeChannelId,
|
|
17
18
|
} from "./channels";
|
|
@@ -22,6 +23,7 @@ interface ChatPaneContentProps {
|
|
|
22
23
|
focused: boolean;
|
|
23
24
|
channelId?: string;
|
|
24
25
|
onChannelChange?: (channelId: string) => void;
|
|
26
|
+
onChannelTitleChange?: (title: string) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function createChatPane(ChatContent: (props: ChatPaneContentProps) => ReactNode) {
|
|
@@ -37,9 +39,30 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
37
39
|
));
|
|
38
40
|
const initialChannelIdRef = useRef(normalizeChannelId(rawPaneChannelId ?? lastVisitedChannelId));
|
|
39
41
|
const persistedChannelId = normalizeChannelId(rawPaneChannelId ?? initialChannelIdRef.current);
|
|
40
|
-
const
|
|
42
|
+
const persistChatPaneState = useCallback((nextChannelId: string, nextTitle?: string) => {
|
|
41
43
|
const currentState = stateRef.current;
|
|
42
|
-
const
|
|
44
|
+
const currentPane = currentState.config.layout.instances.find((instance) => instance.instanceId === paneId);
|
|
45
|
+
const nextTitleValue = nextTitle?.trim() || undefined;
|
|
46
|
+
const currentPaneChannelId = typeof currentPane?.settings?.channelId === "string"
|
|
47
|
+
? normalizeChannelId(currentPane.settings.channelId)
|
|
48
|
+
: null;
|
|
49
|
+
const currentLastVisitedChannelId = currentState.config.pluginConfig["gloomberb-cloud"]?.[LAST_VISITED_CHAT_CHANNEL_KEY];
|
|
50
|
+
if (
|
|
51
|
+
currentPaneChannelId === nextChannelId
|
|
52
|
+
&& (!nextTitleValue || currentPane?.title === nextTitleValue)
|
|
53
|
+
&& currentLastVisitedChannelId === nextChannelId
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const layout = updatePaneInstance(currentState.config.layout, paneId, (instance) => ({
|
|
59
|
+
...instance,
|
|
60
|
+
title: nextTitleValue ?? instance.title,
|
|
61
|
+
settings: {
|
|
62
|
+
...(instance.settings ?? {}),
|
|
63
|
+
channelId: nextChannelId,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
43
66
|
const pluginConfig = {
|
|
44
67
|
...currentState.config.pluginConfig,
|
|
45
68
|
"gloomberb-cloud": {
|
|
@@ -66,8 +89,8 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
66
89
|
|
|
67
90
|
useEffect(() => {
|
|
68
91
|
if (rawPaneChannelId) return;
|
|
69
|
-
|
|
70
|
-
}, [
|
|
92
|
+
persistChatPaneState(initialChannelIdRef.current, formatChatPaneTitle(undefined, initialChannelIdRef.current));
|
|
93
|
+
}, [persistChatPaneState, rawPaneChannelId]);
|
|
71
94
|
|
|
72
95
|
useEffect(() => {
|
|
73
96
|
const normalizedPersisted = normalizeChannelId(persistedChannelId);
|
|
@@ -84,8 +107,12 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
84
107
|
const normalized = normalizeChannelId(nextChannelId);
|
|
85
108
|
pendingChannelIdRef.current = normalized;
|
|
86
109
|
setLocalChannelId((current) => (current === normalized ? current : normalized));
|
|
87
|
-
|
|
88
|
-
}, [
|
|
110
|
+
persistChatPaneState(normalized, formatChatPaneTitle(undefined, normalized));
|
|
111
|
+
}, [persistChatPaneState]);
|
|
112
|
+
|
|
113
|
+
const setChannelTitle = useCallback((nextTitle: string) => {
|
|
114
|
+
persistChatPaneState(channelId, nextTitle);
|
|
115
|
+
}, [channelId, persistChatPaneState]);
|
|
89
116
|
|
|
90
117
|
return (
|
|
91
118
|
<ChatContent
|
|
@@ -94,6 +121,7 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
94
121
|
focused={focused}
|
|
95
122
|
channelId={channelId}
|
|
96
123
|
onChannelChange={setChannelId}
|
|
124
|
+
onChannelTitleChange={setChannelTitle}
|
|
97
125
|
/>
|
|
98
126
|
);
|
|
99
127
|
};
|
|
@@ -442,6 +442,7 @@ describe("ChatContent channel sidebar", () => {
|
|
|
442
442
|
await flushFrame();
|
|
443
443
|
|
|
444
444
|
expect(findPaneInstance(latestState.config.layout, paneInstanceId)?.settings?.channelId).toBe("options");
|
|
445
|
+
expect(findPaneInstance(latestState.config.layout, paneInstanceId)?.title).toBe("#options");
|
|
445
446
|
expect(latestState.config.pluginConfig["gloomberb-cloud"]?.lastChatChannelId).toBe("options");
|
|
446
447
|
expect(setup().captureCharFrame()).toContain("#options");
|
|
447
448
|
} finally {
|
|
@@ -4,12 +4,22 @@ import { colors, hoverBg } from "../../../theme/colors";
|
|
|
4
4
|
import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../ui";
|
|
5
5
|
import { usePluginAppActions } from "../../runtime";
|
|
6
6
|
import { InlineAuthActions } from "../cloud/auth-actions";
|
|
7
|
+
import {
|
|
8
|
+
findChatPaneInstanceForChannel,
|
|
9
|
+
getPreferredChatOpenChannelId,
|
|
10
|
+
} from "./channels";
|
|
7
11
|
import { chatController, type ChatController } from "./controller";
|
|
8
12
|
|
|
9
13
|
interface ChatStatusWidgetProps {
|
|
10
14
|
controller?: Pick<ChatController, "getSnapshot" | "refreshSession" | "subscribe">;
|
|
11
15
|
}
|
|
12
16
|
|
|
17
|
+
type ChatStatusSnapshot = ReturnType<ChatController["getSnapshot"]>;
|
|
18
|
+
|
|
19
|
+
function getTotalUnreadCount(snapshot: ChatStatusSnapshot) {
|
|
20
|
+
return snapshot.channelStates.reduce((total, state) => total + Math.max(0, state.unreadCount), 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
function CloudStatusIcon() {
|
|
14
24
|
const { nativePaneChrome } = useUiCapabilities();
|
|
15
25
|
if (!nativePaneChrome) {
|
|
@@ -43,25 +53,33 @@ function CloudStatusIcon() {
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export function ChatStatusWidget({ controller = chatController }: ChatStatusWidgetProps) {
|
|
46
|
-
const {
|
|
47
|
-
const
|
|
56
|
+
const { createPaneFromTemplate, focusPane } = usePluginAppActions();
|
|
57
|
+
const config = useAppSelector((state) => state.config);
|
|
58
|
+
const cloudPluginDisabled = config.disabledPlugins.includes("gloomberb-cloud");
|
|
48
59
|
const initialSnapshot = controller.getSnapshot();
|
|
49
60
|
const [username, setUsername] = useState<string | null>(initialSnapshot.user?.username ?? null);
|
|
50
61
|
const [hasSavedSession, setHasSavedSession] = useState(initialSnapshot.hasSavedSession);
|
|
51
|
-
const [
|
|
62
|
+
const [snapshot, setSnapshot] = useState(initialSnapshot);
|
|
63
|
+
const unreadCount = getTotalUnreadCount(snapshot);
|
|
52
64
|
const [hovered, setHovered] = useState(false);
|
|
53
65
|
|
|
54
66
|
const openChat = (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
55
67
|
event?.preventDefault?.();
|
|
56
68
|
event?.stopPropagation?.();
|
|
57
|
-
|
|
69
|
+
const channelId = getPreferredChatOpenChannelId(config, snapshot);
|
|
70
|
+
const existingInstance = findChatPaneInstanceForChannel(config, channelId);
|
|
71
|
+
if (existingInstance) {
|
|
72
|
+
focusPane(existingInstance.instanceId);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
createPaneFromTemplate("new-chat-pane", { arg: channelId });
|
|
58
76
|
};
|
|
59
77
|
|
|
60
78
|
useEffect(() => {
|
|
61
|
-
const unsubscribe = controller.subscribe((
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
const unsubscribe = controller.subscribe((nextSnapshot) => {
|
|
80
|
+
setSnapshot(nextSnapshot);
|
|
81
|
+
setUsername(nextSnapshot.user?.username ?? null);
|
|
82
|
+
setHasSavedSession(nextSnapshot.hasSavedSession);
|
|
65
83
|
});
|
|
66
84
|
void controller.refreshSession().catch(() => {});
|
|
67
85
|
return unsubscribe;
|
|
@@ -84,7 +102,7 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
84
102
|
onMouseOut={() => setHovered((current) => (current ? false : current))}
|
|
85
103
|
onMouseDown={openChat}
|
|
86
104
|
>
|
|
87
|
-
<Text fg={
|
|
105
|
+
<Text fg={unreadCount > 0 ? colors.text : colors.textDim}>
|
|
88
106
|
<Span fg={colors.positive}>@</Span>
|
|
89
107
|
{username ? (
|
|
90
108
|
<>
|
|
@@ -93,8 +111,8 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
93
111
|
</>
|
|
94
112
|
) : null}
|
|
95
113
|
</Text>
|
|
96
|
-
{
|
|
97
|
-
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{` [${
|
|
114
|
+
{unreadCount > 0 ? (
|
|
115
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{` [${unreadCount}]`}</Text>
|
|
98
116
|
) : null}
|
|
99
117
|
</Box>
|
|
100
118
|
)}
|
|
@@ -20,6 +20,7 @@ const originalConnectChannel = apiClient.connectChannel.bind(apiClient);
|
|
|
20
20
|
const originalGetChannels = apiClient.getChannels.bind(apiClient);
|
|
21
21
|
const originalGetChatPresence = apiClient.getChatPresence.bind(apiClient);
|
|
22
22
|
const originalUpdateChatChannelState = apiClient.updateChatChannelState.bind(apiClient);
|
|
23
|
+
const originalEditMessage = apiClient.editMessage.bind(apiClient);
|
|
23
24
|
|
|
24
25
|
export type ChatTestSetup = Awaited<ReturnType<typeof testRender>>;
|
|
25
26
|
|
|
@@ -50,6 +51,7 @@ export async function cleanupChatTest(testSetup: ChatTestSetup | undefined): Pro
|
|
|
50
51
|
apiClient.getChannels = originalGetChannels;
|
|
51
52
|
apiClient.getChatPresence = originalGetChatPresence;
|
|
52
53
|
apiClient.updateChatChannelState = originalUpdateChatChannelState;
|
|
54
|
+
apiClient.editMessage = originalEditMessage;
|
|
53
55
|
apiClient.setSessionToken(null);
|
|
54
56
|
|
|
55
57
|
if (testSetup) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { registerTwitterFeedFeature } from "../cloud-tweets";
|
|
14
14
|
import {
|
|
15
15
|
buildDmCommandResults,
|
|
16
|
+
formatChatPaneTitle,
|
|
16
17
|
getLastVisitedChatChannelId,
|
|
17
18
|
hasOnlyDmUsernameArgs,
|
|
18
19
|
normalizeShortcutChannelId,
|
|
@@ -50,8 +51,10 @@ export function createGloomberbCloudPlugin({
|
|
|
50
51
|
const channelId = options?.arg
|
|
51
52
|
? await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(options.arg))
|
|
52
53
|
: await chatController.resolvePreferredChannelId(getLastVisitedChatChannelId(context.config));
|
|
54
|
+
const channel = chatController.getChannels().find((entry) => entry.id === channelId);
|
|
53
55
|
return {
|
|
54
56
|
placement: "floating",
|
|
57
|
+
title: formatChatPaneTitle(channel, channelId),
|
|
55
58
|
settings: { channelId },
|
|
56
59
|
};
|
|
57
60
|
},
|
|
@@ -297,11 +297,13 @@ export function TweetSearchTable({
|
|
|
297
297
|
onBack={() => setDetailOpen(false)}
|
|
298
298
|
detailTitle={selectedTweet ? `@${selectedTweet.author.userName || selectedTweet.author.name} - ${formatTimeAgo(selectedTweet.createdAt)}` : "Tweet"}
|
|
299
299
|
detailContent={selectedTweet ? <TweetDetail tweet={selectedTweet} width={width} onOpenUsername={openUsernameFeed} /> : null}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
300
|
+
selection={{
|
|
301
|
+
kind: "id",
|
|
302
|
+
selectedId: selectedTweetId,
|
|
303
|
+
getId: (tweet) => tweet.id,
|
|
304
|
+
onChange: (id) => setSelectedTweetId(id),
|
|
305
|
+
}}
|
|
306
|
+
onActivate={(tweet) => {
|
|
305
307
|
setSelectedTweetId(tweet.id);
|
|
306
308
|
setDetailOpen(true);
|
|
307
309
|
}}
|
|
@@ -316,12 +318,6 @@ export function TweetSearchTable({
|
|
|
316
318
|
sortDirection={sort.direction}
|
|
317
319
|
onHeaderClick={handleHeaderClick}
|
|
318
320
|
getItemKey={(tweet) => tweet.id}
|
|
319
|
-
isSelected={(tweet) => tweet.id === selectedTweetId}
|
|
320
|
-
onSelect={(tweet) => setSelectedTweetId(tweet.id)}
|
|
321
|
-
onActivate={(tweet) => {
|
|
322
|
-
setSelectedTweetId(tweet.id);
|
|
323
|
-
setDetailOpen(true);
|
|
324
|
-
}}
|
|
325
321
|
renderCell={renderCell}
|
|
326
322
|
emptyContent={emptyContent}
|
|
327
323
|
emptyStateTitle={loading ? "Loading tweets..." : error ?? emptyStateTitle ?? "No tweets"}
|
|
@@ -262,11 +262,13 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
262
262
|
onBack={() => setDetailMode(null)}
|
|
263
263
|
detailTitle={detailTitle}
|
|
264
264
|
detailContent={detailContent}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
265
|
+
selection={{
|
|
266
|
+
kind: "id",
|
|
267
|
+
selectedId: selectedTradeId,
|
|
268
|
+
getId: (trade) => trade.id,
|
|
269
|
+
onChange: (id) => setSelectedTradeId(id),
|
|
270
|
+
}}
|
|
271
|
+
onActivate={(trade) => {
|
|
270
272
|
setSelectedTradeId(trade.id, { immediate: true });
|
|
271
273
|
setDetailMode({ kind: "trade", tradeId: trade.id });
|
|
272
274
|
}}
|
|
@@ -280,12 +282,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
280
282
|
sortDirection={tradeSort.direction}
|
|
281
283
|
onHeaderClick={(columnId) => setTradeSort((current) => nextSort(current, columnId as TradeColumnId, columnId === "member" || columnId === "ticker" ? "asc" : "desc"))}
|
|
282
284
|
getItemKey={(trade) => trade.id}
|
|
283
|
-
isSelected={(trade) => trade.id === selectedTradeId}
|
|
284
|
-
onSelect={(trade) => setSelectedTradeId(trade.id)}
|
|
285
|
-
onActivate={(trade) => {
|
|
286
|
-
setSelectedTradeId(trade.id, { immediate: true });
|
|
287
|
-
setDetailMode({ kind: "trade", tradeId: trade.id });
|
|
288
|
-
}}
|
|
289
285
|
renderCell={renderCongressTradeCell}
|
|
290
286
|
emptyStateTitle="No House PTR trades."
|
|
291
287
|
emptyStateHint="Press r to refresh."
|
|
@@ -297,11 +293,13 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
297
293
|
onBack={() => setDetailMode(null)}
|
|
298
294
|
detailTitle={detailTitle}
|
|
299
295
|
detailContent={detailContent}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
selection={{
|
|
297
|
+
kind: "id",
|
|
298
|
+
selectedId: selectedMemberId,
|
|
299
|
+
getId: (member) => member.id,
|
|
300
|
+
onChange: (id) => setSelectedMemberId(id),
|
|
301
|
+
}}
|
|
302
|
+
onActivate={(member) => {
|
|
305
303
|
setSelectedMemberId(member.id, { immediate: true });
|
|
306
304
|
setDetailMode({ kind: "member", memberId: member.id });
|
|
307
305
|
}}
|
|
@@ -315,12 +313,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
315
313
|
sortDirection={memberSort.direction}
|
|
316
314
|
onHeaderClick={(columnId) => setMemberSort((current) => nextSort(current, columnId as MemberColumnId, columnId === "member" || columnId === "district" ? "asc" : "desc"))}
|
|
317
315
|
getItemKey={(member) => member.id}
|
|
318
|
-
isSelected={(member) => member.id === selectedMemberId}
|
|
319
|
-
onSelect={(member) => setSelectedMemberId(member.id)}
|
|
320
|
-
onActivate={(member) => {
|
|
321
|
-
setSelectedMemberId(member.id, { immediate: true });
|
|
322
|
-
setDetailMode({ kind: "member", memberId: member.id });
|
|
323
|
-
}}
|
|
324
316
|
renderCell={renderCongressMemberCell}
|
|
325
317
|
emptyStateTitle="No House PTR members."
|
|
326
318
|
emptyStateHint="Press r to refresh."
|
|
@@ -132,12 +132,15 @@ function EarningsCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
132
132
|
return (
|
|
133
133
|
<DataTableView<EarningsDisplayRow, EarningsColumn>
|
|
134
134
|
focused={focused}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
selection={{
|
|
136
|
+
kind: "index",
|
|
137
|
+
selectedIndex: selectedRowIndex,
|
|
138
|
+
onChange: (_index, row) => {
|
|
139
|
+
if (row.kind === "event") setSelectedIdx(row.eventIdx);
|
|
140
|
+
},
|
|
139
141
|
}}
|
|
140
|
-
|
|
142
|
+
isNavigable={(row) => row.kind === "event"}
|
|
143
|
+
onActivate={(row) => {
|
|
141
144
|
if (row.kind === "event") openEvent(row.event);
|
|
142
145
|
}}
|
|
143
146
|
onRootKeyDown={handleTableKeyDown}
|
|
@@ -149,8 +152,6 @@ function EarningsCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
149
152
|
sortDirection="asc"
|
|
150
153
|
onHeaderClick={() => {}}
|
|
151
154
|
getItemKey={(row) => row.key}
|
|
152
|
-
isSelected={(row) => row.kind === "event" && row.eventIdx === activeEventIdx}
|
|
153
|
-
onSelect={selectDisplayRow}
|
|
154
155
|
renderSectionHeader={renderEarningsSectionHeader}
|
|
155
156
|
renderCell={renderCell}
|
|
156
157
|
emptyStateTitle={loading ? "Loading earnings..." : "No upcoming earnings found"}
|