gloomberb 0.7.1 → 0.7.3
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 +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +47 -7
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator.ts +8 -1
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/news-wire/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +67 -11
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/ui/host.tsx +8 -2
- package/src/ui/index.tsx +1 -0
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AppNotificationRequest, PluginPersistence, PluginResumeState } from "../../types/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
apiClient,
|
|
4
|
+
type ChatChannel,
|
|
5
|
+
type ChatChannelState,
|
|
6
|
+
type ChatMessage,
|
|
7
|
+
type ChatNotification,
|
|
8
|
+
type PersistedAuthUser,
|
|
9
|
+
} from "../../utils/api-client";
|
|
3
10
|
import { debugLog } from "../../utils/debug-log";
|
|
4
11
|
import { toTimestampMillis } from "../../utils/timestamp";
|
|
5
12
|
|
|
@@ -32,6 +39,7 @@ interface PersistedSessionState {
|
|
|
32
39
|
|
|
33
40
|
interface PersistedChannelState {
|
|
34
41
|
draft: string;
|
|
42
|
+
draftClientMessageId?: string | null;
|
|
35
43
|
replyToId: string | null;
|
|
36
44
|
lastCursor: string | null;
|
|
37
45
|
lastViewedMessageId?: string | null;
|
|
@@ -44,11 +52,13 @@ interface PersistedTranscript {
|
|
|
44
52
|
export interface ChatControllerSnapshot {
|
|
45
53
|
channelId: string;
|
|
46
54
|
channels: ChatChannel[];
|
|
55
|
+
channelStates: ChatChannelState[];
|
|
47
56
|
channelsLoading: boolean;
|
|
48
57
|
loading: boolean;
|
|
49
58
|
loadingOlderMessages: boolean;
|
|
50
59
|
hasOlderMessages: boolean;
|
|
51
60
|
hasSavedSession: boolean;
|
|
61
|
+
onlineCount: number;
|
|
52
62
|
user: { id: string; username: string; emailVerified: boolean } | null;
|
|
53
63
|
messages: ChatMessage[];
|
|
54
64
|
draft: string;
|
|
@@ -56,7 +66,8 @@ export interface ChatControllerSnapshot {
|
|
|
56
66
|
unreadMentionCount: number;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
type ChatConnection = { send: (content: string, replyToId?: string) => Promise<ChatMessage>; close: () => void };
|
|
69
|
+
type ChatConnection = { send: (content: string, replyToId?: string, clientMessageId?: string) => Promise<ChatMessage>; close: () => void };
|
|
70
|
+
type MergeMessagesOptions = { countUnread?: boolean };
|
|
60
71
|
|
|
61
72
|
interface ChannelRuntimeState {
|
|
62
73
|
hydrated: boolean;
|
|
@@ -68,9 +79,12 @@ interface ChannelRuntimeState {
|
|
|
68
79
|
messages: ChatMessage[];
|
|
69
80
|
pendingMessages: ChatMessage[];
|
|
70
81
|
draft: string;
|
|
82
|
+
draftClientMessageId: string | null;
|
|
71
83
|
replyToId: string | null;
|
|
72
84
|
lastCursor: string | null;
|
|
73
85
|
lastViewedMessageId: string | null;
|
|
86
|
+
notificationsEnabled: boolean;
|
|
87
|
+
unreadCount: number;
|
|
74
88
|
wsConnection: ChatConnection | null;
|
|
75
89
|
wsConnected: boolean;
|
|
76
90
|
draftSyncTimer: ReturnType<typeof setTimeout> | null;
|
|
@@ -102,9 +116,12 @@ function createEmptyChannelState(): ChannelRuntimeState {
|
|
|
102
116
|
messages: [],
|
|
103
117
|
pendingMessages: [],
|
|
104
118
|
draft: "",
|
|
119
|
+
draftClientMessageId: null,
|
|
105
120
|
replyToId: null,
|
|
106
121
|
lastCursor: null,
|
|
107
122
|
lastViewedMessageId: null,
|
|
123
|
+
notificationsEnabled: false,
|
|
124
|
+
unreadCount: 0,
|
|
108
125
|
wsConnection: null,
|
|
109
126
|
wsConnected: false,
|
|
110
127
|
draftSyncTimer: null,
|
|
@@ -152,6 +169,23 @@ function formatMentionToast(message: ChatMessage): string {
|
|
|
152
169
|
return `@${author} mentioned you: ${snippet}`;
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
function formatReplyToast(message: ChatMessage): string {
|
|
173
|
+
const author = message.user.username || "Someone";
|
|
174
|
+
const normalized = message.content.replace(/\s+/g, " ").trim();
|
|
175
|
+
if (!normalized) {
|
|
176
|
+
return `@${author} replied to you.`;
|
|
177
|
+
}
|
|
178
|
+
const snippet = normalized.length > 72 ? `${normalized.slice(0, 69)}...` : normalized;
|
|
179
|
+
return `@${author} replied to you: ${snippet}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatChannelToast(channelId: string, message: ChatMessage): string {
|
|
183
|
+
const author = message.user.username || "Someone";
|
|
184
|
+
const normalized = message.content.replace(/\s+/g, " ").trim();
|
|
185
|
+
const snippet = normalized.length > 72 ? `${normalized.slice(0, 69)}...` : normalized;
|
|
186
|
+
return snippet ? `#${channelId} @${author}: ${snippet}` : `#${channelId} @${author} sent a message.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
function isLegacyTimestampCursor(cursor: string | null): boolean {
|
|
156
190
|
return !!cursor && ISO_TIMESTAMP_CURSOR.test(cursor);
|
|
157
191
|
}
|
|
@@ -171,6 +205,11 @@ function resolveHydratedCursor(messages: ChatMessage[], persistedCursor: string
|
|
|
171
205
|
return persistedCursor === transcriptCursor ? persistedCursor : transcriptCursor;
|
|
172
206
|
}
|
|
173
207
|
|
|
208
|
+
function createClientMessageId(): string {
|
|
209
|
+
const randomUUID = globalThis.crypto?.randomUUID?.();
|
|
210
|
+
return randomUUID ?? `local:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
174
213
|
function compareMessages(a: ChatMessage, b: ChatMessage): number {
|
|
175
214
|
// Preserve server/insertion order inside same-timestamp batches so the bottom-most
|
|
176
215
|
// rendered message stays the one selected first from the composer.
|
|
@@ -182,9 +221,11 @@ export class ChatController {
|
|
|
182
221
|
private resume: PluginResumeState | null = null;
|
|
183
222
|
private appActive = true;
|
|
184
223
|
private hydrated = false;
|
|
224
|
+
private sessionToken: string | null = null;
|
|
185
225
|
private sessionChecked = false;
|
|
186
226
|
private user: { id: string; username: string; emailVerified: boolean } | null = null;
|
|
187
227
|
private channels: ChatChannel[] = [];
|
|
228
|
+
private onlineCount = 0;
|
|
188
229
|
private channelsLoading = false;
|
|
189
230
|
private channelsPromise: Promise<void> | null = null;
|
|
190
231
|
private channelStates = new Map<string, ChannelRuntimeState>();
|
|
@@ -193,6 +234,9 @@ export class ChatController {
|
|
|
193
234
|
private pendingMessageSeq = 0;
|
|
194
235
|
private notifyFn: (notification: AppNotificationRequest) => void = () => {};
|
|
195
236
|
private listeners = new Set<ChannelListenerEntry>();
|
|
237
|
+
private chatNotificationUnsubscribe: (() => void) | null = null;
|
|
238
|
+
private chatPresenceUnsubscribe: (() => void) | null = null;
|
|
239
|
+
private notifiedMessageIds = new Set<string>();
|
|
196
240
|
|
|
197
241
|
private get wsConnection(): ChatConnection | null {
|
|
198
242
|
return this.ensureChannelState(DEFAULT_CHAT_CHANNEL_ID).wsConnection;
|
|
@@ -217,7 +261,8 @@ export class ChatController {
|
|
|
217
261
|
}) ?? this.persistence.getState<PersistedSessionState>(SESSION_STATE_KEY, {
|
|
218
262
|
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
219
263
|
});
|
|
220
|
-
|
|
264
|
+
this.sessionToken = session?.sessionToken ?? null;
|
|
265
|
+
apiClient.setSessionToken(this.sessionToken);
|
|
221
266
|
// WebSocket tokens are short-lived connection credentials. Reusing a persisted
|
|
222
267
|
// one can trap reconnects on an expired token even while the session cookie is valid.
|
|
223
268
|
apiClient.setWebSocketToken(null);
|
|
@@ -259,11 +304,13 @@ export class ChatController {
|
|
|
259
304
|
return {
|
|
260
305
|
channelId: normalizedChannelId,
|
|
261
306
|
channels: this.channels,
|
|
307
|
+
channelStates: this.getChannelStateSnapshots(),
|
|
262
308
|
channelsLoading: this.channelsLoading,
|
|
263
309
|
loading: !this.sessionChecked || channel.messagesLoading,
|
|
264
310
|
loadingOlderMessages: channel.olderMessagesLoading,
|
|
265
311
|
hasOlderMessages: channel.messages.length > 0 && !channel.reachedOldestMessage,
|
|
266
|
-
hasSavedSession: !!
|
|
312
|
+
hasSavedSession: !!this.sessionToken,
|
|
313
|
+
onlineCount: this.onlineCount,
|
|
267
314
|
user: this.user,
|
|
268
315
|
messages: this.getVisibleMessages(normalizedChannelId),
|
|
269
316
|
draft: channel.draft,
|
|
@@ -276,6 +323,22 @@ export class ChatController {
|
|
|
276
323
|
return this.channels;
|
|
277
324
|
}
|
|
278
325
|
|
|
326
|
+
private getChannelStateSnapshots(): ChatChannelState[] {
|
|
327
|
+
const channelIds = new Set<string>([
|
|
328
|
+
...this.channels.map((channel) => channel.id),
|
|
329
|
+
...this.channelStates.keys(),
|
|
330
|
+
]);
|
|
331
|
+
return [...channelIds].map((channelId) => {
|
|
332
|
+
const channel = this.ensureChannelState(channelId);
|
|
333
|
+
return {
|
|
334
|
+
channelId,
|
|
335
|
+
notificationsEnabled: channel.notificationsEnabled,
|
|
336
|
+
lastReadMessageId: channel.lastViewedMessageId,
|
|
337
|
+
unreadCount: channel.unreadCount,
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
279
342
|
async refreshChannels(): Promise<void> {
|
|
280
343
|
if (this.channelsPromise) return this.channelsPromise;
|
|
281
344
|
this.channelsLoading = true;
|
|
@@ -298,6 +361,33 @@ export class ChatController {
|
|
|
298
361
|
return request;
|
|
299
362
|
}
|
|
300
363
|
|
|
364
|
+
async refreshPresence(): Promise<void> {
|
|
365
|
+
const presence = await apiClient.getChatPresence();
|
|
366
|
+
this.onlineCount = presence.onlineCount;
|
|
367
|
+
this.emit();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async refreshChatState(): Promise<void> {
|
|
371
|
+
if (!this.user?.emailVerified || !this.sessionToken) {
|
|
372
|
+
await this.refreshPresence();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const state = await apiClient.getChatState();
|
|
376
|
+
this.channels = normalizeChannels(state.channels);
|
|
377
|
+
this.onlineCount = state.onlineCount;
|
|
378
|
+
for (const entry of state.channelStates) {
|
|
379
|
+
const channel = this.ensureChannelState(entry.channelId);
|
|
380
|
+
channel.notificationsEnabled = entry.notificationsEnabled;
|
|
381
|
+
channel.unreadCount = entry.unreadCount;
|
|
382
|
+
channel.lastViewedMessageId = entry.lastReadMessageId ?? channel.lastViewedMessageId;
|
|
383
|
+
}
|
|
384
|
+
for (const notification of state.notifications) {
|
|
385
|
+
this.handleChatNotification(notification, { countUnread: false });
|
|
386
|
+
}
|
|
387
|
+
this.ensureOpenChannelConnections();
|
|
388
|
+
this.emit();
|
|
389
|
+
}
|
|
390
|
+
|
|
301
391
|
async resolveRequiredChannelId(channelId: string): Promise<string> {
|
|
302
392
|
const normalizedChannelId = normalizeChannelId(channelId);
|
|
303
393
|
if (this.isKnownChannelId(normalizedChannelId)) {
|
|
@@ -350,6 +440,9 @@ export class ChatController {
|
|
|
350
440
|
schemaVersion: CHANNEL_SCHEMA_VERSION,
|
|
351
441
|
});
|
|
352
442
|
channel.draft = persistedChannel?.draft ?? "";
|
|
443
|
+
channel.draftClientMessageId = channel.draft.trim()
|
|
444
|
+
? persistedChannel?.draftClientMessageId ?? createClientMessageId()
|
|
445
|
+
: null;
|
|
353
446
|
channel.replyToId = persistedChannel?.replyToId ?? null;
|
|
354
447
|
const persistedCursor = persistedChannel?.lastCursor ?? null;
|
|
355
448
|
const persistedViewedMessageId = persistedChannel?.lastViewedMessageId ?? null;
|
|
@@ -361,22 +454,26 @@ export class ChatController {
|
|
|
361
454
|
});
|
|
362
455
|
channel.messages = transcript?.value.messages ?? [];
|
|
363
456
|
channel.lastCursor = resolveHydratedCursor(channel.messages, persistedCursor);
|
|
364
|
-
channel.lastViewedMessageId = this.user?.id &&
|
|
457
|
+
channel.lastViewedMessageId = this.user?.id && this.sessionToken
|
|
365
458
|
? persistedViewedMessageId ?? getLatestMessageId(channel.messages)
|
|
366
459
|
: null;
|
|
367
460
|
}
|
|
368
461
|
|
|
369
462
|
async refreshSession(): Promise<void> {
|
|
370
463
|
const token = apiClient.getSessionToken();
|
|
464
|
+
this.sessionToken = token;
|
|
371
465
|
if (!token) {
|
|
372
466
|
this.stopVerificationPolling();
|
|
373
467
|
this.stopSafetyRefresh();
|
|
468
|
+
this.stopRealtimeSubscriptions();
|
|
374
469
|
this.closeAllConnections();
|
|
375
470
|
this.user = null;
|
|
376
471
|
this.sessionChecked = true;
|
|
377
472
|
for (const channel of this.channelStates.values()) {
|
|
378
473
|
channel.pendingMessages = [];
|
|
379
474
|
channel.lastViewedMessageId = null;
|
|
475
|
+
channel.unreadCount = 0;
|
|
476
|
+
channel.notificationsEnabled = false;
|
|
380
477
|
}
|
|
381
478
|
this.persistSession();
|
|
382
479
|
this.emit();
|
|
@@ -387,13 +484,17 @@ export class ChatController {
|
|
|
387
484
|
if (!session) {
|
|
388
485
|
this.stopVerificationPolling();
|
|
389
486
|
this.stopSafetyRefresh();
|
|
487
|
+
this.stopRealtimeSubscriptions();
|
|
390
488
|
this.closeAllConnections();
|
|
391
489
|
apiClient.setSessionToken(null);
|
|
490
|
+
this.sessionToken = null;
|
|
392
491
|
this.user = null;
|
|
393
492
|
this.sessionChecked = true;
|
|
394
493
|
for (const channel of this.channelStates.values()) {
|
|
395
494
|
channel.pendingMessages = [];
|
|
396
495
|
channel.lastViewedMessageId = null;
|
|
496
|
+
channel.unreadCount = 0;
|
|
497
|
+
channel.notificationsEnabled = false;
|
|
397
498
|
}
|
|
398
499
|
this.persistSession();
|
|
399
500
|
this.emit();
|
|
@@ -402,6 +503,7 @@ export class ChatController {
|
|
|
402
503
|
|
|
403
504
|
const previousIdentity = `${this.user?.id ?? ""}:${normalizeUsername(this.user?.username) ?? ""}`;
|
|
404
505
|
const nextUser = { id: session.id, username: session.username ?? session.name, emailVerified: !!session.emailVerified };
|
|
506
|
+
this.sessionToken = apiClient.getSessionToken();
|
|
405
507
|
this.user = nextUser;
|
|
406
508
|
const nextIdentity = `${nextUser.id}:${normalizeUsername(nextUser.username) ?? ""}`;
|
|
407
509
|
if (previousIdentity && previousIdentity !== nextIdentity) {
|
|
@@ -416,12 +518,15 @@ export class ChatController {
|
|
|
416
518
|
|
|
417
519
|
if (nextUser?.emailVerified) {
|
|
418
520
|
this.stopVerificationPolling();
|
|
521
|
+
this.ensureRealtimeSubscriptions();
|
|
522
|
+
await this.refreshChatState().catch(() => {});
|
|
419
523
|
this.ensureOpenChannelConnections();
|
|
420
524
|
return;
|
|
421
525
|
}
|
|
422
526
|
|
|
423
527
|
this.syncVerificationPolling();
|
|
424
528
|
this.stopSafetyRefresh();
|
|
529
|
+
this.stopRealtimeSubscriptions();
|
|
425
530
|
this.closeAllConnections();
|
|
426
531
|
}
|
|
427
532
|
|
|
@@ -494,7 +599,13 @@ export class ChatController {
|
|
|
494
599
|
const normalizedChannelId = normalizeChannelId(channelId);
|
|
495
600
|
const channel = this.ensureChannelState(normalizedChannelId);
|
|
496
601
|
if (draft === channel.draft) return;
|
|
602
|
+
const previousDraft = channel.draft;
|
|
497
603
|
channel.draft = draft;
|
|
604
|
+
if (!draft.trim()) {
|
|
605
|
+
channel.draftClientMessageId = null;
|
|
606
|
+
} else if (!previousDraft.trim() || !channel.draftClientMessageId) {
|
|
607
|
+
channel.draftClientMessageId = createClientMessageId();
|
|
608
|
+
}
|
|
498
609
|
this.scheduleDraftSync(normalizedChannelId);
|
|
499
610
|
}
|
|
500
611
|
|
|
@@ -510,6 +621,33 @@ export class ChatController {
|
|
|
510
621
|
this.emit(normalizedChannelId);
|
|
511
622
|
}
|
|
512
623
|
|
|
624
|
+
setChannelNotificationsEnabled(channelId: string, enabled: boolean): void {
|
|
625
|
+
const normalizedChannelId = normalizeChannelId(channelId);
|
|
626
|
+
const channel = this.ensureChannelState(normalizedChannelId);
|
|
627
|
+
if (channel.notificationsEnabled === enabled) return;
|
|
628
|
+
|
|
629
|
+
const previous = channel.notificationsEnabled;
|
|
630
|
+
channel.notificationsEnabled = enabled;
|
|
631
|
+
this.emit();
|
|
632
|
+
this.ensureOpenChannelConnections();
|
|
633
|
+
|
|
634
|
+
void apiClient.updateChatChannelState(normalizedChannelId, {
|
|
635
|
+
notificationsEnabled: enabled,
|
|
636
|
+
}).then((nextState) => {
|
|
637
|
+
this.applyChannelState(nextState);
|
|
638
|
+
this.ensureOpenChannelConnections();
|
|
639
|
+
this.emit();
|
|
640
|
+
}).catch((error) => {
|
|
641
|
+
channel.notificationsEnabled = previous;
|
|
642
|
+
this.ensureOpenChannelConnections();
|
|
643
|
+
this.emit();
|
|
644
|
+
this.notifyFn({
|
|
645
|
+
body: error instanceof Error ? error.message : "Failed to update channel notifications.",
|
|
646
|
+
type: "error",
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
513
651
|
attachView(): () => void {
|
|
514
652
|
return this.attachChannelView(DEFAULT_CHAT_CHANNEL_ID);
|
|
515
653
|
}
|
|
@@ -530,12 +668,19 @@ export class ChatController {
|
|
|
530
668
|
clearSession(): void {
|
|
531
669
|
this.stopVerificationPolling();
|
|
532
670
|
this.stopSafetyRefresh();
|
|
671
|
+
this.stopRealtimeSubscriptions();
|
|
533
672
|
this.closeAllConnections();
|
|
673
|
+
this.sessionToken = null;
|
|
534
674
|
this.user = null;
|
|
535
675
|
this.sessionChecked = false;
|
|
536
676
|
for (const channel of this.channelStates.values()) {
|
|
537
677
|
channel.pendingMessages = [];
|
|
678
|
+
channel.draftClientMessageId = channel.draft.trim()
|
|
679
|
+
? createClientMessageId()
|
|
680
|
+
: null;
|
|
538
681
|
channel.lastViewedMessageId = null;
|
|
682
|
+
channel.unreadCount = 0;
|
|
683
|
+
channel.notificationsEnabled = false;
|
|
539
684
|
}
|
|
540
685
|
this.emit();
|
|
541
686
|
}
|
|
@@ -548,26 +693,32 @@ export class ChatController {
|
|
|
548
693
|
return;
|
|
549
694
|
}
|
|
550
695
|
this.syncVerificationPolling();
|
|
696
|
+
void this.refreshChatState().catch(() => {});
|
|
551
697
|
}
|
|
552
698
|
|
|
553
699
|
reset(clearSession = false): void {
|
|
554
700
|
this.stopVerificationPolling();
|
|
555
701
|
this.stopSafetyRefresh();
|
|
702
|
+
this.stopRealtimeSubscriptions();
|
|
556
703
|
this.closeAllConnections();
|
|
557
704
|
for (const [channelId, channel] of this.channelStates) {
|
|
558
705
|
this.clearDraftSyncTimer(channel);
|
|
559
706
|
channel.messages = [];
|
|
560
707
|
channel.pendingMessages = [];
|
|
561
708
|
channel.draft = "";
|
|
709
|
+
channel.draftClientMessageId = null;
|
|
562
710
|
channel.replyToId = null;
|
|
563
711
|
channel.lastCursor = null;
|
|
564
712
|
channel.lastViewedMessageId = null;
|
|
713
|
+
channel.unreadCount = 0;
|
|
714
|
+
channel.notificationsEnabled = false;
|
|
565
715
|
channel.reachedOldestMessage = false;
|
|
566
716
|
channel.openViewCount = 0;
|
|
567
717
|
this.persistence?.deleteResource(TRANSCRIPT_KIND, channelId, { sourceKey: TRANSCRIPT_SOURCE });
|
|
568
718
|
this.persistChannelState(channelId);
|
|
569
719
|
}
|
|
570
720
|
if (clearSession) {
|
|
721
|
+
this.sessionToken = null;
|
|
571
722
|
this.user = null;
|
|
572
723
|
apiClient.setSessionToken(null);
|
|
573
724
|
this.persistence?.deleteState(SESSION_STATE_KEY);
|
|
@@ -587,6 +738,7 @@ export class ChatController {
|
|
|
587
738
|
}
|
|
588
739
|
this.stopVerificationPolling();
|
|
589
740
|
this.stopSafetyRefresh();
|
|
741
|
+
this.stopRealtimeSubscriptions();
|
|
590
742
|
this.closeAllConnections();
|
|
591
743
|
for (const channel of this.channelStates.values()) {
|
|
592
744
|
channel.messagesLoading = false;
|
|
@@ -597,6 +749,7 @@ export class ChatController {
|
|
|
597
749
|
}
|
|
598
750
|
this.notifyFn = () => {};
|
|
599
751
|
this.listeners.clear();
|
|
752
|
+
this.notifiedMessageIds.clear();
|
|
600
753
|
}
|
|
601
754
|
|
|
602
755
|
send(content: string, replyToId?: string): void {
|
|
@@ -606,8 +759,11 @@ export class ChatController {
|
|
|
606
759
|
sendToChannel(channelId: string, content: string, replyToId?: string): void {
|
|
607
760
|
const normalizedChannelId = normalizeChannelId(channelId);
|
|
608
761
|
const channel = this.ensureChannelState(normalizedChannelId);
|
|
609
|
-
|
|
610
|
-
if (!
|
|
762
|
+
const messageContent = content.trim();
|
|
763
|
+
if (!messageContent) return;
|
|
764
|
+
if (!this.user?.emailVerified || !this.sessionToken) return;
|
|
765
|
+
if (this.hasPendingSend(channel, messageContent, replyToId)) return;
|
|
766
|
+
if (!channel.wsConnection && this.user?.emailVerified && this.sessionToken) {
|
|
611
767
|
this.ensureConnection(normalizedChannelId);
|
|
612
768
|
}
|
|
613
769
|
const connection = channel.wsConnection;
|
|
@@ -616,14 +772,16 @@ export class ChatController {
|
|
|
616
772
|
return;
|
|
617
773
|
}
|
|
618
774
|
|
|
619
|
-
const
|
|
775
|
+
const clientMessageId = channel.draftClientMessageId ?? createClientMessageId();
|
|
776
|
+
const pendingMessage = this.createPendingMessage(normalizedChannelId, messageContent, replyToId);
|
|
620
777
|
channel.pendingMessages = [...channel.pendingMessages, pendingMessage];
|
|
621
778
|
channel.draft = "";
|
|
779
|
+
channel.draftClientMessageId = null;
|
|
622
780
|
channel.replyToId = null;
|
|
623
781
|
this.persistChannelState(normalizedChannelId);
|
|
624
782
|
this.emit(normalizedChannelId);
|
|
625
783
|
|
|
626
|
-
void connection.send(
|
|
784
|
+
void connection.send(messageContent, replyToId, clientMessageId).then((message) => {
|
|
627
785
|
channel.pendingMessages = channel.pendingMessages.filter((entry) => entry.id !== pendingMessage.id);
|
|
628
786
|
this.mergeMessages(normalizedChannelId, [message]);
|
|
629
787
|
}).catch((error) => {
|
|
@@ -638,10 +796,19 @@ export class ChatController {
|
|
|
638
796
|
});
|
|
639
797
|
}
|
|
640
798
|
|
|
799
|
+
private hasPendingSend(channel: ChannelRuntimeState, content: string, replyToId?: string): boolean {
|
|
800
|
+
const replyToKey = replyToId ?? null;
|
|
801
|
+
return channel.pendingMessages.some((message) => (
|
|
802
|
+
message.clientStatus === "sending"
|
|
803
|
+
&& message.content === content
|
|
804
|
+
&& message.replyToId === replyToKey
|
|
805
|
+
));
|
|
806
|
+
}
|
|
807
|
+
|
|
641
808
|
ensureConnection(channelId = DEFAULT_CHAT_CHANNEL_ID): void {
|
|
642
809
|
const normalizedChannelId = normalizeChannelId(channelId);
|
|
643
810
|
const channel = this.ensureChannelState(normalizedChannelId);
|
|
644
|
-
if (!this.user?.emailVerified || !
|
|
811
|
+
if (!this.user?.emailVerified || !this.sessionToken) {
|
|
645
812
|
this.stopSafetyRefresh();
|
|
646
813
|
return;
|
|
647
814
|
}
|
|
@@ -675,8 +842,15 @@ export class ChatController {
|
|
|
675
842
|
}
|
|
676
843
|
}
|
|
677
844
|
|
|
845
|
+
private applyChannelState(state: ChatChannelState): void {
|
|
846
|
+
const channel = this.ensureChannelState(state.channelId);
|
|
847
|
+
channel.notificationsEnabled = state.notificationsEnabled;
|
|
848
|
+
channel.unreadCount = state.unreadCount;
|
|
849
|
+
channel.lastViewedMessageId = state.lastReadMessageId ?? channel.lastViewedMessageId;
|
|
850
|
+
}
|
|
851
|
+
|
|
678
852
|
private syncVerificationPolling(): void {
|
|
679
|
-
if (!this.appActive || !
|
|
853
|
+
if (!this.appActive || !this.sessionToken || !this.user || this.user.emailVerified) {
|
|
680
854
|
this.stopVerificationPolling();
|
|
681
855
|
return;
|
|
682
856
|
}
|
|
@@ -686,6 +860,27 @@ export class ChatController {
|
|
|
686
860
|
}, VERIFICATION_POLL_MS);
|
|
687
861
|
}
|
|
688
862
|
|
|
863
|
+
private ensureRealtimeSubscriptions(): void {
|
|
864
|
+
if (!this.chatNotificationUnsubscribe) {
|
|
865
|
+
this.chatNotificationUnsubscribe = apiClient.subscribeChatNotifications((notification) => {
|
|
866
|
+
this.handleChatNotification(notification);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (!this.chatPresenceUnsubscribe) {
|
|
870
|
+
this.chatPresenceUnsubscribe = apiClient.subscribeChatPresence((onlineCount) => {
|
|
871
|
+
this.onlineCount = onlineCount;
|
|
872
|
+
this.emit();
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private stopRealtimeSubscriptions(): void {
|
|
878
|
+
this.chatNotificationUnsubscribe?.();
|
|
879
|
+
this.chatNotificationUnsubscribe = null;
|
|
880
|
+
this.chatPresenceUnsubscribe?.();
|
|
881
|
+
this.chatPresenceUnsubscribe = null;
|
|
882
|
+
}
|
|
883
|
+
|
|
689
884
|
private stopVerificationPolling(): void {
|
|
690
885
|
if (!this.verificationPollTimer) return;
|
|
691
886
|
clearInterval(this.verificationPollTimer);
|
|
@@ -695,7 +890,7 @@ export class ChatController {
|
|
|
695
890
|
private startSafetyRefresh(): void {
|
|
696
891
|
if (this.safetyRefreshTimer) return;
|
|
697
892
|
this.safetyRefreshTimer = setInterval(() => {
|
|
698
|
-
if (!this.user?.emailVerified || !
|
|
893
|
+
if (!this.user?.emailVerified || !this.sessionToken) {
|
|
699
894
|
this.stopSafetyRefresh();
|
|
700
895
|
return;
|
|
701
896
|
}
|
|
@@ -723,10 +918,16 @@ export class ChatController {
|
|
|
723
918
|
private ensureOpenChannelConnections(): void {
|
|
724
919
|
const channelIds = new Set<string>([DEFAULT_CHAT_CHANNEL_ID]);
|
|
725
920
|
for (const [channelId, channel] of this.channelStates) {
|
|
726
|
-
if (channel.openViewCount > 0 || channel.
|
|
921
|
+
if (channel.openViewCount > 0 || channel.notificationsEnabled) {
|
|
727
922
|
channelIds.add(channelId);
|
|
728
923
|
}
|
|
729
924
|
}
|
|
925
|
+
for (const [channelId, channel] of this.channelStates) {
|
|
926
|
+
if (channelIds.has(channelId) || !channel.wsConnection || channel.openViewCount > 0) continue;
|
|
927
|
+
channel.wsConnection.close();
|
|
928
|
+
channel.wsConnection = null;
|
|
929
|
+
channel.wsConnected = false;
|
|
930
|
+
}
|
|
730
931
|
for (const channelId of channelIds) {
|
|
731
932
|
this.ensureConnection(channelId);
|
|
732
933
|
}
|
|
@@ -734,7 +935,7 @@ export class ChatController {
|
|
|
734
935
|
|
|
735
936
|
private getSafetyRefreshChannelIds(): string[] {
|
|
736
937
|
const active = [...this.channelStates.entries()]
|
|
737
|
-
.filter(([, channel]) => channel.openViewCount > 0 || channel.wsConnection)
|
|
938
|
+
.filter(([, channel]) => channel.openViewCount > 0 || channel.notificationsEnabled || channel.wsConnection)
|
|
738
939
|
.map(([channelId]) => channelId);
|
|
739
940
|
return active.length > 0 ? active : [DEFAULT_CHAT_CHANNEL_ID];
|
|
740
941
|
}
|
|
@@ -743,6 +944,8 @@ export class ChatController {
|
|
|
743
944
|
const channel = this.ensureChannelState(channelId);
|
|
744
945
|
const legacyTimestampCursor = isLegacyTimestampCursor(channel.lastCursor);
|
|
745
946
|
const hasIncrementalCursor = !!channel.lastCursor;
|
|
947
|
+
const hadMessages = channel.messages.length > 0;
|
|
948
|
+
const countIncrementalUnread = hadMessages && hasIncrementalCursor && !legacyTimestampCursor;
|
|
746
949
|
|
|
747
950
|
try {
|
|
748
951
|
const messages = await apiClient.getMessages(channelId, {
|
|
@@ -753,7 +956,7 @@ export class ChatController {
|
|
|
753
956
|
channel.reachedOldestMessage = true;
|
|
754
957
|
}
|
|
755
958
|
if (messages.length > 0) {
|
|
756
|
-
this.mergeMessages(channelId, messages);
|
|
959
|
+
this.mergeMessages(channelId, messages, { countUnread: countIncrementalUnread });
|
|
757
960
|
return;
|
|
758
961
|
}
|
|
759
962
|
if (legacyTimestampCursor) {
|
|
@@ -762,7 +965,7 @@ export class ChatController {
|
|
|
762
965
|
channel.reachedOldestMessage = true;
|
|
763
966
|
}
|
|
764
967
|
if (fullRefresh.length > 0) {
|
|
765
|
-
this.mergeMessages(channelId, fullRefresh);
|
|
968
|
+
this.mergeMessages(channelId, fullRefresh, { countUnread: false });
|
|
766
969
|
return;
|
|
767
970
|
}
|
|
768
971
|
channel.lastCursor = null;
|
|
@@ -775,7 +978,7 @@ export class ChatController {
|
|
|
775
978
|
channel.reachedOldestMessage = true;
|
|
776
979
|
}
|
|
777
980
|
if (messages.length > 0) {
|
|
778
|
-
this.mergeMessages(channelId, messages);
|
|
981
|
+
this.mergeMessages(channelId, messages, { countUnread: false });
|
|
779
982
|
return;
|
|
780
983
|
}
|
|
781
984
|
this.persistChannelState(channelId);
|
|
@@ -795,20 +998,20 @@ export class ChatController {
|
|
|
795
998
|
this.persistChannelState(channelId);
|
|
796
999
|
return;
|
|
797
1000
|
}
|
|
798
|
-
this.mergeMessages(channelId, messages, {
|
|
1001
|
+
this.mergeMessages(channelId, messages, { countUnread: false });
|
|
799
1002
|
}
|
|
800
1003
|
|
|
801
1004
|
private mergeMessages(
|
|
802
1005
|
channelIdOrMessages: string | ChatMessage[],
|
|
803
|
-
maybeMessages?: ChatMessage[] |
|
|
804
|
-
maybeOptions?:
|
|
1006
|
+
maybeMessages?: ChatMessage[] | MergeMessagesOptions,
|
|
1007
|
+
maybeOptions?: MergeMessagesOptions,
|
|
805
1008
|
): void {
|
|
806
1009
|
const channelId = Array.isArray(channelIdOrMessages) ? DEFAULT_CHAT_CHANNEL_ID : channelIdOrMessages;
|
|
807
1010
|
const messages = Array.isArray(channelIdOrMessages)
|
|
808
1011
|
? channelIdOrMessages
|
|
809
1012
|
: (maybeMessages as ChatMessage[] | undefined) ?? [];
|
|
810
1013
|
const options = Array.isArray(channelIdOrMessages)
|
|
811
|
-
? maybeMessages as
|
|
1014
|
+
? maybeMessages as MergeMessagesOptions | undefined
|
|
812
1015
|
: maybeOptions;
|
|
813
1016
|
const channel = this.ensureChannelState(channelId);
|
|
814
1017
|
this.reconcilePendingMessages(channelId, messages);
|
|
@@ -824,11 +1027,11 @@ export class ChatController {
|
|
|
824
1027
|
channel.messages = [...merged.values()]
|
|
825
1028
|
.sort(compareMessages);
|
|
826
1029
|
channel.lastCursor = channel.messages[channel.messages.length - 1]?.id ?? channel.lastCursor;
|
|
1030
|
+
const freshIncoming = [...incoming.values()].filter((message) => message.user.id !== this.user?.id);
|
|
827
1031
|
if (channel.openViewCount > 0) {
|
|
828
1032
|
this.markViewedThroughLatestMessage(channelId, false);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
this.trackUnreadMentions(channelId, [...incoming.values()]);
|
|
1033
|
+
} else if (freshIncoming.length > 0 && options?.countUnread !== false) {
|
|
1034
|
+
channel.unreadCount += freshIncoming.length;
|
|
832
1035
|
}
|
|
833
1036
|
this.persistTranscript(channelId);
|
|
834
1037
|
this.emit(channelId);
|
|
@@ -836,7 +1039,7 @@ export class ChatController {
|
|
|
836
1039
|
|
|
837
1040
|
private persistSession(): void {
|
|
838
1041
|
const value = {
|
|
839
|
-
sessionToken:
|
|
1042
|
+
sessionToken: this.sessionToken,
|
|
840
1043
|
user: this.user,
|
|
841
1044
|
} satisfies PersistedSessionState;
|
|
842
1045
|
this.resume?.setState(SESSION_STATE_KEY, value, {
|
|
@@ -857,6 +1060,7 @@ export class ChatController {
|
|
|
857
1060
|
const channel = this.ensureChannelState(channelId);
|
|
858
1061
|
const value = {
|
|
859
1062
|
draft: channel.draft,
|
|
1063
|
+
draftClientMessageId: channel.draftClientMessageId,
|
|
860
1064
|
replyToId: channel.replyToId,
|
|
861
1065
|
lastCursor: channel.lastCursor,
|
|
862
1066
|
lastViewedMessageId: channel.lastViewedMessageId,
|
|
@@ -911,27 +1115,27 @@ export class ChatController {
|
|
|
911
1115
|
this.persistChannelState(channelId);
|
|
912
1116
|
}
|
|
913
1117
|
|
|
914
|
-
private
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1118
|
+
private handleChatNotification(notification: ChatNotification, options: { countUnread?: boolean } = {}): void {
|
|
1119
|
+
this.mergeMessages(notification.channelId, [notification.message], { countUnread: options.countUnread });
|
|
1120
|
+
const channel = this.ensureChannelState(notification.channelId);
|
|
1121
|
+
if (channel.openViewCount === 0) {
|
|
1122
|
+
this.notifyServerMessage(notification);
|
|
918
1123
|
}
|
|
1124
|
+
void apiClient.markChatNotificationsDelivered([notification.id]).catch(() => {});
|
|
1125
|
+
}
|
|
919
1126
|
|
|
920
|
-
|
|
921
|
-
if (
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
desktop: "when-inactive",
|
|
929
|
-
});
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
1127
|
+
private notifyServerMessage(notification: ChatNotification): void {
|
|
1128
|
+
if (this.notifiedMessageIds.has(notification.messageId)) return;
|
|
1129
|
+
this.notifiedMessageIds.add(notification.messageId);
|
|
1130
|
+
const body = notification.type === "reply"
|
|
1131
|
+
? formatReplyToast(notification.message)
|
|
1132
|
+
: notification.type === "mention"
|
|
1133
|
+
? formatMentionToast(notification.message)
|
|
1134
|
+
: formatChannelToast(notification.channelId, notification.message);
|
|
932
1135
|
this.notifyFn({
|
|
933
1136
|
title: "Gloomberb chat",
|
|
934
|
-
|
|
1137
|
+
subtitle: `#${notification.channelId}`,
|
|
1138
|
+
body,
|
|
935
1139
|
type: "info",
|
|
936
1140
|
desktop: "when-inactive",
|
|
937
1141
|
});
|
|
@@ -966,13 +1170,23 @@ export class ChatController {
|
|
|
966
1170
|
private markViewedThroughLatestMessage(channelId: string, persist = true): boolean {
|
|
967
1171
|
const channel = this.ensureChannelState(channelId);
|
|
968
1172
|
const latestMessageId = channel.messages[channel.messages.length - 1]?.id ?? null;
|
|
969
|
-
|
|
1173
|
+
const previousUnreadCount = channel.unreadCount;
|
|
1174
|
+
if (channel.lastViewedMessageId === latestMessageId && previousUnreadCount === 0) {
|
|
970
1175
|
return false;
|
|
971
1176
|
}
|
|
972
1177
|
channel.lastViewedMessageId = latestMessageId;
|
|
1178
|
+
channel.unreadCount = 0;
|
|
973
1179
|
if (persist) {
|
|
974
1180
|
this.persistChannelState(channelId);
|
|
975
1181
|
}
|
|
1182
|
+
if (latestMessageId && this.user?.emailVerified && this.sessionToken) {
|
|
1183
|
+
void apiClient.updateChatChannelState(channelId, {
|
|
1184
|
+
readThroughMessageId: latestMessageId,
|
|
1185
|
+
}).then((state) => {
|
|
1186
|
+
this.applyChannelState(state);
|
|
1187
|
+
this.emit();
|
|
1188
|
+
}).catch(() => {});
|
|
1189
|
+
}
|
|
976
1190
|
return true;
|
|
977
1191
|
}
|
|
978
1192
|
|
|
@@ -1001,7 +1215,7 @@ export class ChatController {
|
|
|
1001
1215
|
replyTo: replyToMessage
|
|
1002
1216
|
? {
|
|
1003
1217
|
content: replyToMessage.content,
|
|
1004
|
-
user: { username: replyToMessage.user.username },
|
|
1218
|
+
user: { id: replyToMessage.user.id, username: replyToMessage.user.username },
|
|
1005
1219
|
}
|
|
1006
1220
|
: null,
|
|
1007
1221
|
clientStatus: "sending",
|