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.
Files changed (42) hide show
  1. package/README.md +15 -2
  2. package/dist/browser.es.js +1667 -372
  3. package/dist/browser.es.js.map +1 -1
  4. package/dist/browser.full-bundle.min.js +1 -1
  5. package/dist/browser.full-bundle.min.js.map +1 -1
  6. package/dist/browser.js +1668 -371
  7. package/dist/browser.js.map +1 -1
  8. package/dist/index.es.js +1667 -372
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.js +1668 -371
  11. package/dist/index.js.map +1 -1
  12. package/dist/types/channel.d.ts +6 -8
  13. package/dist/types/channel.d.ts.map +1 -1
  14. package/dist/types/channel_state.d.ts +14 -22
  15. package/dist/types/channel_state.d.ts.map +1 -1
  16. package/dist/types/client.d.ts +3 -1
  17. package/dist/types/client.d.ts.map +1 -1
  18. package/dist/types/constants.d.ts +7 -0
  19. package/dist/types/constants.d.ts.map +1 -0
  20. package/dist/types/index.d.ts +2 -0
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/store.d.ts +14 -0
  23. package/dist/types/store.d.ts.map +1 -0
  24. package/dist/types/thread.d.ts +93 -29
  25. package/dist/types/thread.d.ts.map +1 -1
  26. package/dist/types/thread_manager.d.ts +51 -0
  27. package/dist/types/thread_manager.d.ts.map +1 -0
  28. package/dist/types/types.d.ts +35 -18
  29. package/dist/types/types.d.ts.map +1 -1
  30. package/dist/types/utils.d.ts +48 -7
  31. package/dist/types/utils.d.ts.map +1 -1
  32. package/package.json +7 -6
  33. package/src/channel.ts +28 -13
  34. package/src/channel_state.ts +30 -27
  35. package/src/client.ts +182 -104
  36. package/src/constants.ts +4 -0
  37. package/src/index.ts +2 -0
  38. package/src/store.ts +57 -0
  39. package/src/thread.ts +470 -107
  40. package/src/thread_manager.ts +297 -0
  41. package/src/types.ts +34 -19
  42. 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 type ThreadResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
502
- channel: ChannelResponse<StreamChatGenerics>;
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
- deleted_at: string;
506
- latest_replies: MessageResponse<StreamChatGenerics>[];
507
- parent_message: MessageResponse<StreamChatGenerics>;
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;