stream-chat 8.38.0 → 8.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/browser.es.js +1667 -372
- package/dist/browser.es.js.map +1 -1
- package/dist/browser.full-bundle.min.js +1 -1
- package/dist/browser.full-bundle.min.js.map +1 -1
- package/dist/browser.js +1668 -371
- package/dist/browser.js.map +1 -1
- package/dist/index.es.js +1667 -372
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1668 -371
- package/dist/index.js.map +1 -1
- package/dist/types/channel.d.ts +6 -8
- package/dist/types/channel.d.ts.map +1 -1
- package/dist/types/channel_state.d.ts +14 -22
- package/dist/types/channel_state.d.ts.map +1 -1
- package/dist/types/client.d.ts +3 -1
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/constants.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/store.d.ts +14 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/thread.d.ts +93 -29
- package/dist/types/thread.d.ts.map +1 -1
- package/dist/types/thread_manager.d.ts +51 -0
- package/dist/types/thread_manager.d.ts.map +1 -0
- package/dist/types/types.d.ts +35 -18
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +48 -7
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/channel.ts +28 -13
- package/src/channel_state.ts +30 -27
- package/src/client.ts +182 -104
- package/src/constants.ts +4 -0
- package/src/index.ts +2 -0
- package/src/store.ts +57 -0
- package/src/thread.ts +470 -107
- package/src/thread_manager.ts +297 -0
- package/src/types.ts +34 -19
- package/src/utils.ts +362 -43
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { StateStore } from './store';
|
|
2
|
+
import { throttle } from './utils';
|
|
3
|
+
|
|
4
|
+
import type { StreamChat } from './client';
|
|
5
|
+
import type { Thread } from './thread';
|
|
6
|
+
import type { DefaultGenerics, Event, ExtendableGenerics, OwnUserResponse, QueryThreadsOptions } from './types';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONNECTION_RECOVERY_THROTTLE_DURATION = 1000;
|
|
9
|
+
const MAX_QUERY_THREADS_LIMIT = 25;
|
|
10
|
+
|
|
11
|
+
export type ThreadManagerState<SCG extends ExtendableGenerics = DefaultGenerics> = {
|
|
12
|
+
active: boolean;
|
|
13
|
+
isThreadOrderStale: boolean;
|
|
14
|
+
lastConnectionDropAt: Date | null;
|
|
15
|
+
pagination: ThreadManagerPagination;
|
|
16
|
+
ready: boolean;
|
|
17
|
+
threads: Thread<SCG>[];
|
|
18
|
+
unreadThreadCount: number;
|
|
19
|
+
/**
|
|
20
|
+
* List of threads that haven't been loaded in the list, but have received new messages
|
|
21
|
+
* since the latest reload. Useful to display a banner prompting to reload the thread list.
|
|
22
|
+
*/
|
|
23
|
+
unseenThreadIds: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ThreadManagerPagination = {
|
|
27
|
+
isLoading: boolean;
|
|
28
|
+
isLoadingNext: boolean;
|
|
29
|
+
nextCursor: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class ThreadManager<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
33
|
+
public readonly state: StateStore<ThreadManagerState<SCG>>;
|
|
34
|
+
private client: StreamChat<SCG>;
|
|
35
|
+
private unsubscribeFunctions: Set<() => void> = new Set();
|
|
36
|
+
private threadsByIdGetterCache: {
|
|
37
|
+
threads: ThreadManagerState<SCG>['threads'];
|
|
38
|
+
threadsById: Record<string, Thread<SCG> | undefined>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
constructor({ client }: { client: StreamChat<SCG> }) {
|
|
42
|
+
this.client = client;
|
|
43
|
+
this.state = new StateStore<ThreadManagerState<SCG>>({
|
|
44
|
+
active: false,
|
|
45
|
+
isThreadOrderStale: false,
|
|
46
|
+
threads: [],
|
|
47
|
+
unreadThreadCount: 0,
|
|
48
|
+
unseenThreadIds: [],
|
|
49
|
+
lastConnectionDropAt: null,
|
|
50
|
+
pagination: {
|
|
51
|
+
isLoading: false,
|
|
52
|
+
isLoadingNext: false,
|
|
53
|
+
nextCursor: null,
|
|
54
|
+
},
|
|
55
|
+
ready: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.threadsByIdGetterCache = { threads: [], threadsById: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public get threadsById() {
|
|
62
|
+
const { threads } = this.state.getLatestValue();
|
|
63
|
+
|
|
64
|
+
if (threads === this.threadsByIdGetterCache.threads) {
|
|
65
|
+
return this.threadsByIdGetterCache.threadsById;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const threadsById = threads.reduce<Record<string, Thread<SCG>>>((newThreadsById, thread) => {
|
|
69
|
+
newThreadsById[thread.id] = thread;
|
|
70
|
+
return newThreadsById;
|
|
71
|
+
}, {});
|
|
72
|
+
|
|
73
|
+
this.threadsByIdGetterCache.threads = threads;
|
|
74
|
+
this.threadsByIdGetterCache.threadsById = threadsById;
|
|
75
|
+
|
|
76
|
+
return threadsById;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public activate = () => {
|
|
80
|
+
this.state.partialNext({ active: true });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
public deactivate = () => {
|
|
84
|
+
this.state.partialNext({ active: false });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
public registerSubscriptions = () => {
|
|
88
|
+
if (this.unsubscribeFunctions.size) return;
|
|
89
|
+
|
|
90
|
+
this.unsubscribeFunctions.add(this.subscribeUnreadThreadsCountChange());
|
|
91
|
+
this.unsubscribeFunctions.add(this.subscribeManageThreadSubscriptions());
|
|
92
|
+
this.unsubscribeFunctions.add(this.subscribeReloadOnActivation());
|
|
93
|
+
this.unsubscribeFunctions.add(this.subscribeNewReplies());
|
|
94
|
+
this.unsubscribeFunctions.add(this.subscribeRecoverAfterConnectionDrop());
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
private subscribeUnreadThreadsCountChange = () => {
|
|
98
|
+
// initiate
|
|
99
|
+
const { unread_threads: unreadThreadCount = 0 } = (this.client.user as OwnUserResponse<SCG>) ?? {};
|
|
100
|
+
this.state.partialNext({ unreadThreadCount });
|
|
101
|
+
|
|
102
|
+
const unsubscribeFunctions = [
|
|
103
|
+
'health.check',
|
|
104
|
+
'notification.mark_read',
|
|
105
|
+
'notification.thread_message_new',
|
|
106
|
+
'notification.channel_deleted',
|
|
107
|
+
].map(
|
|
108
|
+
(eventType) =>
|
|
109
|
+
this.client.on(eventType, (event) => {
|
|
110
|
+
const { unread_threads: unreadThreadCount } = event.me ?? event;
|
|
111
|
+
if (typeof unreadThreadCount === 'number') {
|
|
112
|
+
this.state.partialNext({ unreadThreadCount });
|
|
113
|
+
}
|
|
114
|
+
}).unsubscribe,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
private subscribeManageThreadSubscriptions = () =>
|
|
121
|
+
this.state.subscribeWithSelector(
|
|
122
|
+
(nextValue) => [nextValue.threads] as const,
|
|
123
|
+
([nextThreads], prev) => {
|
|
124
|
+
const [prevThreads = []] = prev ?? [];
|
|
125
|
+
// Thread instance was removed if there's no thread with the given id at all,
|
|
126
|
+
// or it was replaced with a new instance
|
|
127
|
+
const removedThreads = prevThreads.filter((thread) => thread !== this.threadsById[thread.id]);
|
|
128
|
+
|
|
129
|
+
nextThreads.forEach((thread) => thread.registerSubscriptions());
|
|
130
|
+
removedThreads.forEach((thread) => thread.unregisterSubscriptions());
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
private subscribeReloadOnActivation = () =>
|
|
135
|
+
this.state.subscribeWithSelector(
|
|
136
|
+
(nextValue) => [nextValue.active],
|
|
137
|
+
([active]) => {
|
|
138
|
+
if (active) this.reload();
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
private subscribeNewReplies = () =>
|
|
143
|
+
this.client.on('notification.thread_message_new', (event: Event<SCG>) => {
|
|
144
|
+
const parentId = event.message?.parent_id;
|
|
145
|
+
if (!parentId) return;
|
|
146
|
+
|
|
147
|
+
const { unseenThreadIds, ready } = this.state.getLatestValue();
|
|
148
|
+
if (!ready) return;
|
|
149
|
+
|
|
150
|
+
if (this.threadsById[parentId]) {
|
|
151
|
+
this.state.partialNext({ isThreadOrderStale: true });
|
|
152
|
+
} else if (!unseenThreadIds.includes(parentId)) {
|
|
153
|
+
this.state.partialNext({ unseenThreadIds: unseenThreadIds.concat(parentId) });
|
|
154
|
+
}
|
|
155
|
+
}).unsubscribe;
|
|
156
|
+
|
|
157
|
+
private subscribeRecoverAfterConnectionDrop = () => {
|
|
158
|
+
const unsubscribeConnectionDropped = this.client.on('connection.changed', (event) => {
|
|
159
|
+
if (event.online === false) {
|
|
160
|
+
this.state.next((current) =>
|
|
161
|
+
current.lastConnectionDropAt
|
|
162
|
+
? current
|
|
163
|
+
: {
|
|
164
|
+
...current,
|
|
165
|
+
lastConnectionDropAt: new Date(),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}).unsubscribe;
|
|
170
|
+
|
|
171
|
+
const throttledHandleConnectionRecovered = throttle(
|
|
172
|
+
() => {
|
|
173
|
+
const { lastConnectionDropAt } = this.state.getLatestValue();
|
|
174
|
+
if (!lastConnectionDropAt) return;
|
|
175
|
+
this.reload({ force: true });
|
|
176
|
+
},
|
|
177
|
+
DEFAULT_CONNECTION_RECOVERY_THROTTLE_DURATION,
|
|
178
|
+
{ trailing: true },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const unsubscribeConnectionRecovered = this.client.on('connection.recovered', throttledHandleConnectionRecovered)
|
|
182
|
+
.unsubscribe;
|
|
183
|
+
|
|
184
|
+
return () => {
|
|
185
|
+
unsubscribeConnectionDropped();
|
|
186
|
+
unsubscribeConnectionRecovered();
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
public unregisterSubscriptions = () => {
|
|
191
|
+
this.state.getLatestValue().threads.forEach((thread) => thread.unregisterSubscriptions());
|
|
192
|
+
this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction());
|
|
193
|
+
this.unsubscribeFunctions.clear();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
public reload = async ({ force = false } = {}) => {
|
|
197
|
+
const { threads, unseenThreadIds, isThreadOrderStale, pagination, ready } = this.state.getLatestValue();
|
|
198
|
+
if (pagination.isLoading) return;
|
|
199
|
+
if (!force && ready && !unseenThreadIds.length && !isThreadOrderStale) return;
|
|
200
|
+
const limit = threads.length + unseenThreadIds.length;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
this.state.next((current) => ({
|
|
204
|
+
...current,
|
|
205
|
+
pagination: {
|
|
206
|
+
...current.pagination,
|
|
207
|
+
isLoading: true,
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
const response = await this.queryThreads({ limit: Math.min(limit, MAX_QUERY_THREADS_LIMIT) });
|
|
212
|
+
|
|
213
|
+
const currentThreads = this.threadsById;
|
|
214
|
+
const nextThreads: Thread<SCG>[] = [];
|
|
215
|
+
|
|
216
|
+
for (const incomingThread of response.threads) {
|
|
217
|
+
const existingThread = currentThreads[incomingThread.id];
|
|
218
|
+
|
|
219
|
+
if (existingThread) {
|
|
220
|
+
// Reuse thread instances if possible
|
|
221
|
+
nextThreads.push(existingThread);
|
|
222
|
+
if (existingThread.hasStaleState) {
|
|
223
|
+
existingThread.hydrateState(incomingThread);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
nextThreads.push(incomingThread);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.state.next((current) => ({
|
|
231
|
+
...current,
|
|
232
|
+
threads: nextThreads,
|
|
233
|
+
unseenThreadIds: [],
|
|
234
|
+
isThreadOrderStale: false,
|
|
235
|
+
pagination: {
|
|
236
|
+
...current.pagination,
|
|
237
|
+
isLoading: false,
|
|
238
|
+
nextCursor: response.next ?? null,
|
|
239
|
+
},
|
|
240
|
+
ready: true,
|
|
241
|
+
}));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.client.logger('error', (error as Error).message);
|
|
244
|
+
this.state.next((current) => ({
|
|
245
|
+
...current,
|
|
246
|
+
pagination: {
|
|
247
|
+
...current.pagination,
|
|
248
|
+
isLoading: false,
|
|
249
|
+
},
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
public queryThreads = (options: QueryThreadsOptions = {}) => {
|
|
255
|
+
return this.client.queryThreads({
|
|
256
|
+
limit: 25,
|
|
257
|
+
participant_limit: 10,
|
|
258
|
+
reply_limit: 10,
|
|
259
|
+
watch: true,
|
|
260
|
+
...options,
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
public loadNextPage = async (options: Omit<QueryThreadsOptions, 'next'> = {}) => {
|
|
265
|
+
const { pagination } = this.state.getLatestValue();
|
|
266
|
+
|
|
267
|
+
if (pagination.isLoadingNext || !pagination.nextCursor) return;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
this.state.partialNext({ pagination: { ...pagination, isLoadingNext: true } });
|
|
271
|
+
|
|
272
|
+
const response = await this.queryThreads({
|
|
273
|
+
...options,
|
|
274
|
+
next: pagination.nextCursor,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
this.state.next((current) => ({
|
|
278
|
+
...current,
|
|
279
|
+
threads: response.threads.length ? current.threads.concat(response.threads) : current.threads,
|
|
280
|
+
pagination: {
|
|
281
|
+
...current.pagination,
|
|
282
|
+
nextCursor: response.next ?? null,
|
|
283
|
+
isLoadingNext: false,
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
this.client.logger('error', (error as Error).message);
|
|
288
|
+
this.state.next((current) => ({
|
|
289
|
+
...current,
|
|
290
|
+
pagination: {
|
|
291
|
+
...current.pagination,
|
|
292
|
+
isLoadingNext: false,
|
|
293
|
+
},
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -473,10 +473,11 @@ export type FormatMessageResponse<StreamChatGenerics extends ExtendableGenerics
|
|
|
473
473
|
reactionType: StreamChatGenerics['reactionType'];
|
|
474
474
|
userType: StreamChatGenerics['userType'];
|
|
475
475
|
}>,
|
|
476
|
-
'created_at' | 'pinned_at' | 'updated_at' | 'status'
|
|
476
|
+
'created_at' | 'pinned_at' | 'updated_at' | 'deleted_at' | 'status'
|
|
477
477
|
> &
|
|
478
478
|
StreamChatGenerics['messageType'] & {
|
|
479
479
|
created_at: Date;
|
|
480
|
+
deleted_at: Date | null;
|
|
480
481
|
pinned_at: Date | null;
|
|
481
482
|
status: string;
|
|
482
483
|
updated_at: Date;
|
|
@@ -498,28 +499,34 @@ export type GetMessageAPIResponse<
|
|
|
498
499
|
StreamChatGenerics extends ExtendableGenerics = DefaultGenerics
|
|
499
500
|
> = SendMessageAPIResponse<StreamChatGenerics>;
|
|
500
501
|
|
|
501
|
-
export
|
|
502
|
-
|
|
502
|
+
export interface ThreadResponse<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
503
|
+
// FIXME: according to OpenAPI, `channel` could be undefined but since cid is provided I'll asume that it's wrong
|
|
504
|
+
channel: ChannelResponse<SCG>;
|
|
503
505
|
channel_cid: string;
|
|
504
506
|
created_at: string;
|
|
505
|
-
|
|
506
|
-
latest_replies: MessageResponse<
|
|
507
|
-
parent_message: MessageResponse<
|
|
507
|
+
created_by_user_id: string;
|
|
508
|
+
latest_replies: Array<MessageResponse<SCG>>;
|
|
509
|
+
parent_message: MessageResponse<SCG>;
|
|
508
510
|
parent_message_id: string;
|
|
509
|
-
read: {
|
|
510
|
-
last_read: string;
|
|
511
|
-
last_read_message_id: string;
|
|
512
|
-
unread_messages: number;
|
|
513
|
-
user: UserResponse<StreamChatGenerics>;
|
|
514
|
-
}[];
|
|
515
|
-
reply_count: number;
|
|
516
|
-
thread_participants: {
|
|
517
|
-
created_at: string;
|
|
518
|
-
user: UserResponse<StreamChatGenerics>;
|
|
519
|
-
}[];
|
|
520
511
|
title: string;
|
|
521
512
|
updated_at: string;
|
|
522
|
-
|
|
513
|
+
created_by?: UserResponse<SCG>;
|
|
514
|
+
deleted_at?: string;
|
|
515
|
+
last_message_at?: string;
|
|
516
|
+
participant_count?: number;
|
|
517
|
+
read?: Array<ReadResponse<SCG>>;
|
|
518
|
+
reply_count?: number;
|
|
519
|
+
thread_participants?: Array<{
|
|
520
|
+
channel_cid: string;
|
|
521
|
+
created_at: string;
|
|
522
|
+
last_read_at: string;
|
|
523
|
+
last_thread_message_at?: string;
|
|
524
|
+
left_thread_at?: string;
|
|
525
|
+
thread_id?: string;
|
|
526
|
+
user?: UserResponse<SCG>;
|
|
527
|
+
user_id?: string;
|
|
528
|
+
}>;
|
|
529
|
+
}
|
|
523
530
|
|
|
524
531
|
// TODO: Figure out a way to strongly type set and unset.
|
|
525
532
|
export type PartialThreadUpdate = {
|
|
@@ -1052,7 +1059,7 @@ export type PaginationOptions = {
|
|
|
1052
1059
|
id_lt?: string;
|
|
1053
1060
|
id_lte?: string;
|
|
1054
1061
|
limit?: number;
|
|
1055
|
-
offset?: number;
|
|
1062
|
+
offset?: number; // should be avoided with channel.query()
|
|
1056
1063
|
};
|
|
1057
1064
|
|
|
1058
1065
|
export type MessagePaginationOptions = PaginationOptions & {
|
|
@@ -1236,6 +1243,8 @@ export type Event<StreamChatGenerics extends ExtendableGenerics = DefaultGeneric
|
|
|
1236
1243
|
unread_count?: number;
|
|
1237
1244
|
// number of unread messages in the channel from this event (notification.mark_unread)
|
|
1238
1245
|
unread_messages?: number;
|
|
1246
|
+
unread_thread_messages?: number;
|
|
1247
|
+
unread_threads?: number;
|
|
1239
1248
|
user?: UserResponse<StreamChatGenerics>;
|
|
1240
1249
|
user_id?: string;
|
|
1241
1250
|
watcher_count?: number;
|
|
@@ -2900,6 +2909,12 @@ export type ImportTask = {
|
|
|
2900
2909
|
};
|
|
2901
2910
|
|
|
2902
2911
|
export type MessageSetType = 'latest' | 'current' | 'new';
|
|
2912
|
+
export type MessageSet<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
|
|
2913
|
+
isCurrent: boolean;
|
|
2914
|
+
isLatest: boolean;
|
|
2915
|
+
messages: FormatMessageResponse<StreamChatGenerics>[];
|
|
2916
|
+
pagination: { hasNext: boolean; hasPrev: boolean };
|
|
2917
|
+
};
|
|
2903
2918
|
|
|
2904
2919
|
export type PushProviderUpsertResponse = {
|
|
2905
2920
|
push_provider: PushProvider;
|