stream-chat 8.15.0 → 8.17.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/src/client.ts CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  GetImportResponse,
96
96
  GetMessageAPIResponse,
97
97
  GetRateLimitsResponse,
98
+ QueryThreadsAPIResponse,
98
99
  GetUnreadCountAPIResponse,
99
100
  GetUnreadCountBatchAPIResponse,
100
101
  ListChannelResponse,
@@ -163,8 +164,13 @@ import {
163
164
  UserOptions,
164
165
  UserResponse,
165
166
  UserSort,
167
+ GetThreadAPIResponse,
168
+ PartialThreadUpdate,
169
+ QueryThreadsOptions,
170
+ GetThreadOptions,
166
171
  } from './types';
167
172
  import { InsightMetrics, postInsights } from './insights';
173
+ import { Thread } from './thread';
168
174
 
169
175
  function isString(x: unknown): x is string {
170
176
  return typeof x === 'string' || x instanceof String;
@@ -2595,6 +2601,103 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
2595
2601
  );
2596
2602
  }
2597
2603
 
2604
+ /**
2605
+ * queryThreads - returns the list of threads of current user.
2606
+ *
2607
+ * @param {QueryThreadsOptions} options Options object for pagination and limiting the participants and replies.
2608
+ * @param {number} options.limit Limits the number of threads to be returned.
2609
+ * @param {boolean} options.watch Subscribes the user to the channels of the threads.
2610
+ * @param {number} options.participant_limit Limits the number of participants returned per threads.
2611
+ * @param {number} options.reply_limit Limits the number of replies returned per threads.
2612
+ *
2613
+ * @returns {{ threads: Thread<StreamChatGenerics>[], next: string }} Returns the list of threads and the next cursor.
2614
+ */
2615
+ async queryThreads(options?: QueryThreadsOptions) {
2616
+ const opts = {
2617
+ limit: 10,
2618
+ participant_limit: 10,
2619
+ reply_limit: 3,
2620
+ watch: true,
2621
+ ...options,
2622
+ };
2623
+
2624
+ const res = await this.post<QueryThreadsAPIResponse<StreamChatGenerics>>(this.baseURL + `/threads`, opts);
2625
+
2626
+ return {
2627
+ threads: res.threads.map((thread) => new Thread(this, thread)),
2628
+ next: res.next,
2629
+ };
2630
+ }
2631
+
2632
+ /**
2633
+ * getThread - returns the thread of a message by its id.
2634
+ *
2635
+ * @param {string} messageId The message id
2636
+ * @param {GetThreadOptions} options Options object for pagination and limiting the participants and replies.
2637
+ * @param {boolean} options.watch Subscribes the user to the channel of the thread.
2638
+ * @param {number} options.participant_limit Limits the number of participants returned per threads.
2639
+ * @param {number} options.reply_limit Limits the number of replies returned per threads.
2640
+ *
2641
+ * @returns {Thread<StreamChatGenerics>} Returns the thread.
2642
+ */
2643
+ async getThread(messageId: string, options: GetThreadOptions = {}) {
2644
+ if (!messageId) {
2645
+ throw Error('Please specify the message id when calling partialUpdateThread');
2646
+ }
2647
+
2648
+ const opts = {
2649
+ participant_limit: 100,
2650
+ reply_limit: 3,
2651
+ watch: true,
2652
+ ...options,
2653
+ };
2654
+
2655
+ const res = await this.get<GetThreadAPIResponse<StreamChatGenerics>>(this.baseURL + `/threads/${messageId}`, opts);
2656
+
2657
+ return new Thread<StreamChatGenerics>(this, res.thread);
2658
+ }
2659
+
2660
+ /**
2661
+ * partialUpdateThread - updates the given thread
2662
+ *
2663
+ * @param {string} messageId The id of the thread message which needs to be updated.
2664
+ * @param {PartialThreadUpdate} partialThreadObject should contain "set" or "unset" params for any of the thread's non-reserved fields.
2665
+ *
2666
+ * @returns {GetThreadAPIResponse<StreamChatGenerics>} Returns the updated thread.
2667
+ */
2668
+ async partialUpdateThread(messageId: string, partialThreadObject: PartialThreadUpdate) {
2669
+ if (!messageId) {
2670
+ throw Error('Please specify the message id when calling partialUpdateThread');
2671
+ }
2672
+
2673
+ // check for reserved fields from ThreadResponse type within partialThreadObject's set and unset.
2674
+ // Throw error if any of the reserved field is found.
2675
+ const reservedThreadFields = [
2676
+ 'created_at',
2677
+ 'id',
2678
+ 'last_message_at',
2679
+ 'type',
2680
+ 'updated_at',
2681
+ 'user',
2682
+ 'reply_count',
2683
+ 'participants',
2684
+ 'channel',
2685
+ ];
2686
+
2687
+ for (const key in { ...partialThreadObject.set, ...partialThreadObject.unset }) {
2688
+ if (reservedThreadFields.includes(key)) {
2689
+ throw Error(
2690
+ `You cannot set ${key} field on Thread object. ${key} is reserved for server-side use. Please omit ${key} from your set object.`,
2691
+ );
2692
+ }
2693
+ }
2694
+
2695
+ return await this.patch<GetThreadAPIResponse<StreamChatGenerics>>(
2696
+ this.baseURL + `/threads/${messageId}`,
2697
+ partialThreadObject,
2698
+ );
2699
+ }
2700
+
2598
2701
  getUserAgent() {
2599
2702
  return (
2600
2703
  this.userAgent || `stream-chat-javascript-client-${this.node ? 'node' : 'browser'}-${process.env.PKG_VERSION}`
@@ -2938,6 +3041,7 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
2938
3041
  async querySegments(filter: {}, options: QuerySegmentsOptions = {}) {
2939
3042
  return await this.get<{
2940
3043
  segments: Segment[];
3044
+ next?: string;
2941
3045
  }>(this.baseURL + `/segments`, {
2942
3046
  payload: {
2943
3047
  filter,
package/src/events.ts CHANGED
@@ -28,6 +28,7 @@ export const EVENT_MAP = {
28
28
  'notification.message_new': true,
29
29
  'notification.mutes_updated': true,
30
30
  'notification.removed_from_channel': true,
31
+ 'notification.thread_message_new': true,
31
32
  'reaction.deleted': true,
32
33
  'reaction.new': true,
33
34
  'reaction.updated': true,
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './client';
3
3
  export * from './client_state';
4
4
  export * from './channel';
5
5
  export * from './channel_state';
6
+ export * from './thread';
6
7
  export * from './connection';
7
8
  export * from './events';
8
9
  export * from './permissions';
@@ -10,4 +11,4 @@ export * from './signing';
10
11
  export * from './token_manager';
11
12
  export * from './insights';
12
13
  export * from './types';
13
- export { isOwnUser, chatCodes, logChatPromiseExecution } from './utils';
14
+ export { isOwnUser, chatCodes, logChatPromiseExecution, formatMessage } from './utils';
package/src/thread.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { StreamChat } from './client';
2
+ import {
3
+ DefaultGenerics,
4
+ ExtendableGenerics,
5
+ MessageResponse,
6
+ ThreadResponse,
7
+ ChannelResponse,
8
+ FormatMessageResponse,
9
+ ReactionResponse,
10
+ UserResponse,
11
+ } from './types';
12
+ import { addToMessageList, formatMessage } from './utils';
13
+
14
+ type ThreadReadStatus<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = Record<
15
+ string,
16
+ {
17
+ last_read: Date;
18
+ last_read_message_id: string;
19
+ unread_messages: number;
20
+ user: UserResponse<StreamChatGenerics>;
21
+ }
22
+ >;
23
+
24
+ export class Thread<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> {
25
+ id: string;
26
+ latestReplies: FormatMessageResponse<StreamChatGenerics>[] = [];
27
+ participants: ThreadResponse['thread_participants'] = [];
28
+ message: FormatMessageResponse<StreamChatGenerics>;
29
+ channel: ChannelResponse<StreamChatGenerics>;
30
+ _channel: ReturnType<StreamChat<StreamChatGenerics>['channel']>;
31
+ replyCount = 0;
32
+ _client: StreamChat<StreamChatGenerics>;
33
+ read: ThreadReadStatus<StreamChatGenerics> = {};
34
+
35
+ constructor(client: StreamChat<StreamChatGenerics>, t: ThreadResponse<StreamChatGenerics>) {
36
+ this.id = t.parent_message.id;
37
+ this.message = formatMessage(t.parent_message);
38
+ this.latestReplies = t.latest_replies.map(formatMessage);
39
+ this.participants = t.thread_participants;
40
+ this.replyCount = t.reply_count;
41
+ this.channel = t.channel;
42
+ this._channel = client.channel(t.channel.type, t.channel.id);
43
+ this._client = client;
44
+ for (const r of t.read) {
45
+ this.read[r.user.id] = {
46
+ ...r,
47
+ last_read: new Date(r.last_read),
48
+ };
49
+ }
50
+ }
51
+
52
+ getClient(): StreamChat<StreamChatGenerics> {
53
+ return this._client;
54
+ }
55
+
56
+ addReply(message: MessageResponse<StreamChatGenerics>) {
57
+ this.latestReplies = addToMessageList(this.latestReplies, formatMessage(message));
58
+ }
59
+
60
+ updateReply(message: MessageResponse<StreamChatGenerics>) {
61
+ this.latestReplies = this.latestReplies.map((m) => {
62
+ if (m.id === message.id) {
63
+ return formatMessage(message);
64
+ }
65
+ return m;
66
+ });
67
+ }
68
+
69
+ updateMessageOrReplyIfExists(message: MessageResponse<StreamChatGenerics>) {
70
+ if (!message.parent_id && message.id !== this.message.id) {
71
+ return;
72
+ }
73
+
74
+ if (message.parent_id && message.parent_id !== this.message.id) {
75
+ return;
76
+ }
77
+
78
+ if (message.parent_id && message.parent_id === this.message.id) {
79
+ this.updateReply(message);
80
+ }
81
+
82
+ if (!message.parent_id && message.id === this.message.id) {
83
+ this.message = formatMessage(message);
84
+ }
85
+ }
86
+
87
+ addReaction(
88
+ reaction: ReactionResponse<StreamChatGenerics>,
89
+ message?: MessageResponse<StreamChatGenerics>,
90
+ enforce_unique?: boolean,
91
+ ) {
92
+ if (!message) return;
93
+
94
+ this.latestReplies = this.latestReplies.map((m) => {
95
+ if (m.id === message.id) {
96
+ return formatMessage(
97
+ this._channel.state.addReaction(reaction, message, enforce_unique) as MessageResponse<StreamChatGenerics>,
98
+ );
99
+ }
100
+ return m;
101
+ });
102
+ }
103
+
104
+ removeReaction(reaction: ReactionResponse<StreamChatGenerics>, message?: MessageResponse<StreamChatGenerics>) {
105
+ if (!message) return;
106
+
107
+ this.latestReplies = this.latestReplies.map((m) => {
108
+ if (m.id === message.id) {
109
+ return formatMessage(
110
+ this._channel.state.removeReaction(reaction, message) as MessageResponse<StreamChatGenerics>,
111
+ );
112
+ }
113
+ return m;
114
+ });
115
+ }
116
+ }
package/src/types.ts CHANGED
@@ -478,6 +478,58 @@ export type GetMessageAPIResponse<
478
478
  StreamChatGenerics extends ExtendableGenerics = DefaultGenerics
479
479
  > = SendMessageAPIResponse<StreamChatGenerics>;
480
480
 
481
+ export type ThreadResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
482
+ channel: ChannelResponse<StreamChatGenerics>;
483
+ channel_cid: string;
484
+ created_at: string;
485
+ deleted_at: string;
486
+ latest_replies: MessageResponse<StreamChatGenerics>[];
487
+ parent_message: MessageResponse<StreamChatGenerics>;
488
+ parent_message_id: string;
489
+ read: {
490
+ last_read: string;
491
+ last_read_message_id: string;
492
+ unread_messages: number;
493
+ user: UserResponse<StreamChatGenerics>;
494
+ }[];
495
+ reply_count: number;
496
+ thread_participants: {
497
+ created_at: string;
498
+ user: UserResponse<StreamChatGenerics>;
499
+ }[];
500
+ title: string;
501
+ updated_at: string;
502
+ };
503
+
504
+ // TODO: Figure out a way to strongly type set and unset.
505
+ export type PartialThreadUpdate = {
506
+ set?: Partial<Record<string, unknown>>;
507
+ unset?: Partial<Record<string, unknown>>;
508
+ };
509
+
510
+ export type QueryThreadsOptions = {
511
+ limit?: number;
512
+ next?: string;
513
+ participant_limit?: number;
514
+ reply_limit?: number;
515
+ watch?: boolean;
516
+ };
517
+
518
+ export type QueryThreadsAPIResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = APIResponse & {
519
+ threads: ThreadResponse<StreamChatGenerics>[];
520
+ next?: string;
521
+ };
522
+
523
+ export type GetThreadOptions = {
524
+ participant_limit?: number;
525
+ reply_limit?: number;
526
+ watch?: boolean;
527
+ };
528
+
529
+ export type GetThreadAPIResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = APIResponse & {
530
+ thread: ThreadResponse<StreamChatGenerics>;
531
+ };
532
+
481
533
  export type GetMultipleMessagesAPIResponse<
482
534
  StreamChatGenerics extends ExtendableGenerics = DefaultGenerics
483
535
  > = APIResponse & {
@@ -510,7 +562,14 @@ export type GetUnreadCountAPIResponse = APIResponse & {
510
562
  last_read: string;
511
563
  unread_count: number;
512
564
  }[];
565
+ threads: {
566
+ last_read: string;
567
+ last_read_message_id: string;
568
+ parent_message_id: string;
569
+ unread_count: number;
570
+ }[];
513
571
  total_unread_count: number;
572
+ total_unread_threads_count: number;
514
573
  };
515
574
 
516
575
  export type GetUnreadCountBatchAPIResponse = APIResponse & {
@@ -893,6 +952,7 @@ export type MarkChannelsReadOptions<StreamChatGenerics extends ExtendableGeneric
893
952
  export type MarkReadOptions<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
894
953
  client_id?: string;
895
954
  connection_id?: string;
955
+ thread_id?: string;
896
956
  user?: UserResponse<StreamChatGenerics>;
897
957
  user_id?: string;
898
958
  };
@@ -1099,6 +1159,7 @@ export type Event<StreamChatGenerics extends ExtendableGenerics = DefaultGeneric
1099
1159
  reaction?: ReactionResponse<StreamChatGenerics>;
1100
1160
  received_at?: string | Date;
1101
1161
  team?: string;
1162
+ thread?: ThreadResponse<StreamChatGenerics>;
1102
1163
  // @deprecated number of all unread messages across all current user's unread channels, equals unread_count
1103
1164
  total_unread_count?: number;
1104
1165
  // number of all current user's channels with at least one unread message including the channel in this event
@@ -1446,6 +1507,7 @@ export type UserFilters<StreamChatGenerics extends ExtendableGenerics = DefaultG
1446
1507
  | RequireOnlyOne<{
1447
1508
  $contains?: PrimitiveFilter<string>;
1448
1509
  $eq?: PrimitiveFilter<UserResponse<StreamChatGenerics>['teams']>;
1510
+ $in?: PrimitiveFilter<UserResponse<StreamChatGenerics>['teams']>;
1449
1511
  }>
1450
1512
  | PrimitiveFilter<UserResponse<StreamChatGenerics>['teams']>;
1451
1513
  username?:
@@ -2425,8 +2487,8 @@ export type DeleteUserOptions = {
2425
2487
  export type SegmentType = 'channel' | 'user';
2426
2488
 
2427
2489
  export type SegmentData = {
2428
- description: string;
2429
- filter: {};
2490
+ description?: string;
2491
+ filter?: {};
2430
2492
  };
2431
2493
 
2432
2494
  export type Segment = {
package/src/utils.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import FormData from 'form-data';
2
- import { AscDesc, ExtendableGenerics, DefaultGenerics, OwnUserBase, OwnUserResponse, UserResponse } from './types';
2
+ import {
3
+ AscDesc,
4
+ ExtendableGenerics,
5
+ DefaultGenerics,
6
+ OwnUserBase,
7
+ OwnUserResponse,
8
+ UserResponse,
9
+ MessageResponse,
10
+ FormatMessageResponse,
11
+ } from './types';
3
12
  import { AxiosRequestConfig } from 'axios';
4
13
 
5
14
  /**
@@ -263,3 +272,94 @@ export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (pa
263
272
 
264
273
  return newParams.join('&');
265
274
  };
275
+
276
+ /**
277
+ * formatMessage - Takes the message object. Parses the dates, sets __html
278
+ * and sets the status to received if missing. Returns a message object
279
+ *
280
+ * @param {MessageResponse<StreamChatGenerics>} message a message object
281
+ *
282
+ */
283
+ export function formatMessage<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
284
+ message: MessageResponse<StreamChatGenerics>,
285
+ ): FormatMessageResponse<StreamChatGenerics> {
286
+ return {
287
+ ...message,
288
+ /**
289
+ * @deprecated please use `html`
290
+ */
291
+ __html: message.html,
292
+ // parse the date..
293
+ pinned_at: message.pinned_at ? new Date(message.pinned_at) : null,
294
+ created_at: message.created_at ? new Date(message.created_at) : new Date(),
295
+ updated_at: message.updated_at ? new Date(message.updated_at) : new Date(),
296
+ status: message.status || 'received',
297
+ };
298
+ }
299
+
300
+ export function addToMessageList<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
301
+ messages: Array<FormatMessageResponse<StreamChatGenerics>>,
302
+ message: FormatMessageResponse<StreamChatGenerics>,
303
+ timestampChanged = false,
304
+ sortBy: 'pinned_at' | 'created_at' = 'created_at',
305
+ addIfDoesNotExist = true,
306
+ ) {
307
+ const addMessageToList = addIfDoesNotExist || timestampChanged;
308
+ let messageArr = messages;
309
+
310
+ // if created_at has changed, message should be filtered and re-inserted in correct order
311
+ // slow op but usually this only happens for a message inserted to state before actual response with correct timestamp
312
+ if (timestampChanged) {
313
+ messageArr = messageArr.filter((msg) => !(msg.id && message.id === msg.id));
314
+ }
315
+
316
+ // Get array length after filtering
317
+ const messageArrayLength = messageArr.length;
318
+
319
+ // for empty list just concat and return unless it's an update or deletion
320
+ if (messageArrayLength === 0 && addMessageToList) {
321
+ return messageArr.concat(message);
322
+ } else if (messageArrayLength === 0) {
323
+ return [...messageArr];
324
+ }
325
+
326
+ const messageTime = (message[sortBy] as Date).getTime();
327
+ const messageIsNewest = (messageArr[messageArrayLength - 1][sortBy] as Date).getTime() < messageTime;
328
+
329
+ // if message is newer than last item in the list concat and return unless it's an update or deletion
330
+ if (messageIsNewest && addMessageToList) {
331
+ return messageArr.concat(message);
332
+ } else if (messageIsNewest) {
333
+ return [...messageArr];
334
+ }
335
+
336
+ // find the closest index to push the new message
337
+ let left = 0;
338
+ let middle = 0;
339
+ let right = messageArrayLength - 1;
340
+ while (left <= right) {
341
+ middle = Math.floor((right + left) / 2);
342
+ if ((messageArr[middle][sortBy] as Date).getTime() <= messageTime) left = middle + 1;
343
+ else right = middle - 1;
344
+ }
345
+
346
+ // message already exists and not filtered due to timestampChanged, update and return
347
+ if (!timestampChanged && message.id) {
348
+ if (messageArr[left] && message.id === messageArr[left].id) {
349
+ messageArr[left] = message;
350
+ return [...messageArr];
351
+ }
352
+
353
+ if (messageArr[left - 1] && message.id === messageArr[left - 1].id) {
354
+ messageArr[left - 1] = message;
355
+ return [...messageArr];
356
+ }
357
+ }
358
+
359
+ // Do not add updated or deleted messages to the list if they do not already exist
360
+ // or have a timestamp change.
361
+ if (addMessageToList) {
362
+ messageArr.splice(left, 0, message);
363
+ }
364
+ return [...messageArr];
365
+ }