@ermis-network/ermis-chat-sdk 1.0.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.
@@ -0,0 +1,580 @@
1
+ import { Channel } from './channel';
2
+ import {
3
+ ChannelMemberResponse,
4
+ ChannelMembership,
5
+ FormatMessageResponse,
6
+ Event,
7
+ ExtendableGenerics,
8
+ DefaultGenerics,
9
+ MessageSetType,
10
+ MessageResponse,
11
+ ReactionResponse,
12
+ UserResponse,
13
+ } from './types';
14
+ import { addToMessageList } from './utils';
15
+
16
+ type ChannelReadStatus<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> = Record<
17
+ string,
18
+ {
19
+ last_read: Date;
20
+ unread_messages: number;
21
+ user: UserResponse<ErmisChatGenerics>;
22
+ last_read_message_id?: string;
23
+ last_send?: string;
24
+ }
25
+ >;
26
+
27
+ /**
28
+ * ChannelState - A container class for the channel state.
29
+ */
30
+ export class ChannelState<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> {
31
+ _channel: Channel<ErmisChatGenerics>;
32
+ watcher_count: number;
33
+ typing: Record<string, Event<ErmisChatGenerics>>;
34
+ read: ChannelReadStatus<ErmisChatGenerics>;
35
+ pinnedMessages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>;
36
+ watchers: Record<string, UserResponse<ErmisChatGenerics>>;
37
+ members: Record<string, ChannelMemberResponse<ErmisChatGenerics>>;
38
+ unreadCount: number;
39
+ membership: ChannelMembership<ErmisChatGenerics>;
40
+ last_message_at: Date | null;
41
+ isUpToDate: boolean;
42
+ messageSets: {
43
+ isCurrent: boolean;
44
+ isLatest: boolean;
45
+ messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>;
46
+ }[] = [];
47
+ topics?: Channel<ErmisChatGenerics>[] = [];
48
+ constructor(channel: Channel<ErmisChatGenerics>) {
49
+ this._channel = channel;
50
+ this.watcher_count = 0;
51
+ this.typing = {};
52
+ this.read = {};
53
+ this.initMessages();
54
+ this.pinnedMessages = [];
55
+ this.watchers = {};
56
+ this.members = {};
57
+ this.membership = {};
58
+ this.unreadCount = 0;
59
+ this.isUpToDate = true;
60
+ this.last_message_at = channel?.state?.last_message_at != null ? new Date(channel.state.last_message_at) : null;
61
+ }
62
+
63
+ get messages() {
64
+ return this.messageSets.find((s) => s.isCurrent)?.messages || [];
65
+ }
66
+
67
+ set messages(messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>) {
68
+ const index = this.messageSets.findIndex((s) => s.isCurrent);
69
+ this.messageSets[index].messages = messages;
70
+ }
71
+
72
+ get latestMessages() {
73
+ return this.messageSets.find((s) => s.isLatest)?.messages || [];
74
+ }
75
+
76
+ set latestMessages(messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>) {
77
+ const index = this.messageSets.findIndex((s) => s.isLatest);
78
+ this.messageSets[index].messages = messages;
79
+ }
80
+
81
+ addMessageSorted(
82
+ newMessage: MessageResponse<ErmisChatGenerics>,
83
+ timestampChanged = false,
84
+ addIfDoesNotExist = true,
85
+ messageSetToAddToIfDoesNotExist: MessageSetType = 'latest',
86
+ ) {
87
+ return this.addMessagesSorted(
88
+ [newMessage],
89
+ timestampChanged,
90
+ false,
91
+ addIfDoesNotExist,
92
+ messageSetToAddToIfDoesNotExist,
93
+ );
94
+ }
95
+
96
+ formatMessage(message: MessageResponse<ErmisChatGenerics>): FormatMessageResponse<ErmisChatGenerics> {
97
+ return {
98
+ ...message,
99
+ /**
100
+ * @deprecated please use `html`
101
+ */
102
+ __html: message.html,
103
+ // parse the date..
104
+ pinned_at: message.pinned_at ? new Date(message.pinned_at) : null,
105
+ created_at: message.created_at ? new Date(message.created_at) : new Date(),
106
+ updated_at: message.updated_at ? new Date(message.updated_at) : null,
107
+ status: message.status || 'received',
108
+ };
109
+ }
110
+
111
+ addMessagesSorted(
112
+ newMessages: MessageResponse<ErmisChatGenerics>[],
113
+ timestampChanged = false,
114
+ initializing = false,
115
+ addIfDoesNotExist = true,
116
+ messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
117
+ ) {
118
+ const { messagesToAdd, targetMessageSetIndex } = this.findTargetMessageSet(
119
+ newMessages,
120
+ addIfDoesNotExist,
121
+ messageSetToAddToIfDoesNotExist,
122
+ );
123
+
124
+ for (let i = 0; i < messagesToAdd.length; i += 1) {
125
+ // If message is already formatted we can skip the tasks below
126
+ // This will be true for messages that are already present at the state -> this happens when we perform merging of message sets
127
+ // This will be also true for message previews used by some SDKs
128
+ const isMessageFormatted = messagesToAdd[i].created_at instanceof Date;
129
+ let message: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>;
130
+ if (isMessageFormatted) {
131
+ message = messagesToAdd[i] as ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>;
132
+ } else {
133
+ message = this.formatMessage(messagesToAdd[i] as MessageResponse<ErmisChatGenerics>);
134
+
135
+ if (message.user && this._channel?.cid) {
136
+ /**
137
+ * Store the reference to user for this channel, so that when we have to
138
+ * handle updates to user, we can use the reference map, to determine which
139
+ * channels need to be updated with updated user object.
140
+ */
141
+ this._channel.getClient().state.updateUserReference(message.user, this._channel.cid);
142
+ }
143
+
144
+ if (!this.last_message_at) {
145
+ this.last_message_at = new Date(message.created_at.getTime());
146
+ }
147
+
148
+ if (message.created_at.getTime() > this.last_message_at.getTime()) {
149
+ this.last_message_at = new Date(message.created_at.getTime());
150
+ }
151
+ }
152
+
153
+ // Cross-reference with pinnedMessages to ensure `pinned` and `pinned_at` are accurate
154
+ if (!message.pinned) {
155
+ const pinnedMatch = this.pinnedMessages.find((pm) => pm.id === message.id);
156
+ if (pinnedMatch) {
157
+ message.pinned = true;
158
+ message.pinned_at = pinnedMatch.pinned_at || new Date();
159
+ }
160
+ }
161
+
162
+ // update or append the messages...
163
+ const parentID = message.parent_id;
164
+
165
+ // add to the given message set
166
+ if (!parentID && targetMessageSetIndex !== -1) {
167
+ this.messageSets[targetMessageSetIndex].messages = this._addToMessageList(
168
+ this.messageSets[targetMessageSetIndex].messages,
169
+ message,
170
+ timestampChanged,
171
+ 'created_at',
172
+ addIfDoesNotExist,
173
+ );
174
+ }
175
+ }
176
+
177
+ return {
178
+ messageSet: this.messageSets[targetMessageSetIndex],
179
+ };
180
+ }
181
+
182
+ addPinnedMessages(pinnedMessages: MessageResponse<ErmisChatGenerics>[]) {
183
+ for (let i = 0; i < pinnedMessages.length; i += 1) {
184
+ this.addPinnedMessage(pinnedMessages[i]);
185
+ }
186
+ // Sort by pinned_at descending (newest pin first)
187
+ this.pinnedMessages.sort((a, b) => {
188
+ const timeA = a.pinned_at ? new Date(a.pinned_at).getTime() : 0;
189
+ const timeB = b.pinned_at ? new Date(b.pinned_at).getTime() : 0;
190
+ return timeB - timeA;
191
+ });
192
+ }
193
+
194
+ addPinnedMessage(pinnedMessage: MessageResponse<ErmisChatGenerics>) {
195
+ const formatted = this.formatMessage(pinnedMessage);
196
+ // Remove existing entry if present (to avoid duplicates)
197
+ this.pinnedMessages = this.pinnedMessages.filter((msg) => msg.id !== formatted.id);
198
+ // Add to the beginning of the list (newest pin first)
199
+ this.pinnedMessages = [formatted, ...this.pinnedMessages];
200
+ }
201
+
202
+ removePinnedMessage(message: MessageResponse<ErmisChatGenerics>) {
203
+ const { result } = this.removeMessageFromArray(this.pinnedMessages, message);
204
+ this.pinnedMessages = result;
205
+ }
206
+
207
+ addReaction(
208
+ reaction: ReactionResponse<ErmisChatGenerics>,
209
+ message?: MessageResponse<ErmisChatGenerics>,
210
+ enforce_unique?: boolean,
211
+ ) {
212
+ if (!message) return;
213
+ const messageWithReaction = message;
214
+ this._updateMessage(message, (msg) => {
215
+ messageWithReaction.own_reactions = this._addOwnReactionToMessage(msg.own_reactions, reaction, enforce_unique);
216
+ return this.formatMessage(messageWithReaction);
217
+ });
218
+ return messageWithReaction;
219
+ }
220
+
221
+ _addOwnReactionToMessage(
222
+ ownReactions: ReactionResponse<ErmisChatGenerics>[] | null | undefined,
223
+ reaction: ReactionResponse<ErmisChatGenerics>,
224
+ enforce_unique?: boolean,
225
+ ) {
226
+ if (enforce_unique) {
227
+ ownReactions = [];
228
+ } else {
229
+ ownReactions = this._removeOwnReactionFromMessage(ownReactions, reaction);
230
+ }
231
+
232
+ ownReactions = ownReactions || [];
233
+ if (this._channel.getClient().userID === reaction.user_id) {
234
+ ownReactions.push(reaction);
235
+ }
236
+
237
+ return ownReactions;
238
+ }
239
+
240
+ _removeOwnReactionFromMessage(
241
+ ownReactions: ReactionResponse<ErmisChatGenerics>[] | null | undefined,
242
+ reaction: ReactionResponse<ErmisChatGenerics>,
243
+ ) {
244
+ if (ownReactions) {
245
+ return ownReactions.filter((item) => item.user_id !== reaction.user_id || item.type !== reaction.type);
246
+ }
247
+ return ownReactions;
248
+ }
249
+
250
+ removeReaction(reaction: ReactionResponse<ErmisChatGenerics>, message?: MessageResponse<ErmisChatGenerics>) {
251
+ if (!message) return;
252
+ const messageWithReaction = message;
253
+ this._updateMessage(message, (msg) => {
254
+ messageWithReaction.own_reactions = this._removeOwnReactionFromMessage(msg.own_reactions, reaction);
255
+ return this.formatMessage(messageWithReaction);
256
+ });
257
+ return messageWithReaction;
258
+ }
259
+
260
+ removeQuotedMessageReferences(message: MessageResponse<ErmisChatGenerics>) {
261
+ const parseMessage = (m: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>) =>
262
+ ({
263
+ ...m,
264
+ created_at: m.created_at.toISOString(),
265
+ pinned_at: m.pinned_at?.toISOString(),
266
+ updated_at: m.updated_at?.toISOString(),
267
+ } as unknown as MessageResponse<ErmisChatGenerics>);
268
+
269
+ this.messageSets.forEach((set) => {
270
+ const updatedMessages = set.messages
271
+ .filter((msg) => msg.quoted_message_id === message.id)
272
+ .map(parseMessage)
273
+ .map((msg) => ({ ...msg, quoted_message: { ...message, attachments: [] } }));
274
+
275
+ this.addMessagesSorted(updatedMessages, true);
276
+ });
277
+ }
278
+
279
+ _updateMessage(
280
+ message: {
281
+ id?: string;
282
+ parent_id?: string;
283
+ pinned?: boolean;
284
+ },
285
+ updateFunc: (
286
+ msg: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>,
287
+ ) => ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>,
288
+ ) {
289
+ const { parent_id, pinned } = message;
290
+
291
+ if (!parent_id) {
292
+ const messageSetIndex = this.findMessageSetIndex(message);
293
+ if (messageSetIndex !== -1) {
294
+ const msgIndex = this.messageSets[messageSetIndex].messages.findIndex((msg) => msg.id === message.id);
295
+ if (msgIndex !== -1) {
296
+ this.messageSets[messageSetIndex].messages[msgIndex] = updateFunc(
297
+ this.messageSets[messageSetIndex].messages[msgIndex],
298
+ );
299
+ }
300
+ }
301
+ }
302
+
303
+ if (pinned) {
304
+ const msgIndex = this.pinnedMessages.findIndex((msg) => msg.id === message.id);
305
+ if (msgIndex !== -1) {
306
+ this.pinnedMessages[msgIndex] = updateFunc(this.pinnedMessages[msgIndex]);
307
+ }
308
+ }
309
+ }
310
+
311
+ setIsUpToDate = (isUpToDate: boolean) => {
312
+ this.isUpToDate = isUpToDate;
313
+ };
314
+
315
+ /**
316
+ * Update the status of a message by ID (used for optimistic UI).
317
+ */
318
+ updateMessageStatus(messageId: string, status: string) {
319
+ this._updateMessage({ id: messageId }, (msg) => ({
320
+ ...msg,
321
+ status,
322
+ }));
323
+ }
324
+
325
+ _addToMessageList(
326
+ messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
327
+ message: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>,
328
+ timestampChanged = false,
329
+ sortBy: 'pinned_at' | 'created_at' = 'created_at',
330
+ addIfDoesNotExist = true,
331
+ ) {
332
+ return addToMessageList(messages, message, timestampChanged, sortBy, addIfDoesNotExist);
333
+ }
334
+
335
+ removeMessage(messageToRemove: { id: string; messageSetIndex?: number; parent_id?: string }) {
336
+ let isRemoved = false;
337
+ const messageSetIndex = messageToRemove.messageSetIndex ?? this.findMessageSetIndex(messageToRemove);
338
+ if (messageSetIndex !== -1) {
339
+ const { removed, result: messages } = this.removeMessageFromArray(
340
+ this.messageSets[messageSetIndex].messages,
341
+ messageToRemove,
342
+ );
343
+ this.messageSets[messageSetIndex].messages = messages;
344
+ isRemoved = removed;
345
+ }
346
+
347
+ return isRemoved;
348
+ }
349
+
350
+ removeMessageFromArray = (
351
+ msgArray: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
352
+ msg: { id: string; parent_id?: string },
353
+ ) => {
354
+ const result = msgArray.filter((message) => !(!!message.id && !!msg.id && message.id === msg.id));
355
+
356
+ return { removed: result.length < msgArray.length, result };
357
+ };
358
+
359
+ updateUserMessages = (user: UserResponse<ErmisChatGenerics>) => {
360
+ const _updateUserMessages = (
361
+ messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
362
+ user: UserResponse<ErmisChatGenerics>,
363
+ ) => {
364
+ for (let i = 0; i < messages.length; i++) {
365
+ const m = messages[i];
366
+ const latestReactions = m?.latest_reactions || [];
367
+ if (m.user?.id === user.id) {
368
+ messages[i] = {
369
+ ...m,
370
+ user: m.user?.id === user.id ? user : m.user,
371
+ };
372
+ }
373
+
374
+ if (latestReactions && latestReactions.some((r) => r.user?.id === user.id)) {
375
+ messages[i] = {
376
+ ...m,
377
+ latest_reactions: latestReactions.map((r) => (r.user?.id === user.id ? { ...r, user } : r)),
378
+ };
379
+ }
380
+ }
381
+ };
382
+
383
+ this.messageSets.forEach((set) => _updateUserMessages(set.messages, user));
384
+
385
+ _updateUserMessages(this.pinnedMessages, user);
386
+ };
387
+
388
+ deleteUserMessages = (user: UserResponse<ErmisChatGenerics>, hardDelete = false) => {
389
+ const _deleteUserMessages = (
390
+ messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
391
+ user: UserResponse<ErmisChatGenerics>,
392
+ hardDelete = false,
393
+ ) => {
394
+ for (let i = 0; i < messages.length; i++) {
395
+ const m = messages[i];
396
+ if (m.user?.id !== user.id) {
397
+ continue;
398
+ }
399
+
400
+ if (hardDelete) {
401
+ /**
402
+ * In case of hard delete, we need to strip down all text, html,
403
+ * attachments and all the custom properties on message
404
+ */
405
+ messages[i] = {
406
+ cid: m.cid,
407
+ created_at: m.created_at,
408
+ deleted_at: new Date().toISOString(),
409
+ id: m.id,
410
+ latest_reactions: [],
411
+ mentioned_users: [],
412
+ own_reactions: [],
413
+ parent_id: m.parent_id,
414
+ reply_count: m.reply_count,
415
+ status: m.status,
416
+ type: 'deleted',
417
+ updated_at: m.updated_at,
418
+ user: m.user,
419
+ } as unknown as ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>;
420
+ } else {
421
+ messages[i] = {
422
+ ...m,
423
+ type: 'deleted',
424
+ deleted_at: new Date().toISOString(),
425
+ };
426
+ }
427
+ }
428
+ };
429
+
430
+ this.messageSets.forEach((set) => _deleteUserMessages(set.messages, user, hardDelete));
431
+
432
+ _deleteUserMessages(this.pinnedMessages, user, hardDelete);
433
+ };
434
+
435
+ filterErrorMessages() {
436
+ const filteredMessages = this.latestMessages.filter((message) => message.type !== 'error');
437
+
438
+ this.latestMessages = filteredMessages;
439
+ }
440
+
441
+ clean() {
442
+ const now = new Date();
443
+ // prevent old users from showing up as typing
444
+ for (const [userID, lastEvent] of Object.entries(this.typing)) {
445
+ const receivedAt =
446
+ typeof lastEvent.received_at === 'string'
447
+ ? new Date(lastEvent.received_at)
448
+ : lastEvent.received_at || new Date();
449
+ if (now.getTime() - receivedAt.getTime() > 7000) {
450
+ delete this.typing[userID];
451
+ this._channel.getClient().dispatchEvent({
452
+ cid: this._channel.cid,
453
+ type: 'typing.stop',
454
+ user: { id: userID },
455
+ } as Event<ErmisChatGenerics>);
456
+ }
457
+ }
458
+ }
459
+
460
+ clearMessages() {
461
+ this.initMessages();
462
+ this.pinnedMessages = [];
463
+ }
464
+
465
+ initMessages() {
466
+ this.messageSets = [{ messages: [], isLatest: true, isCurrent: true }];
467
+ }
468
+
469
+ async loadMessageIntoState(messageId: string | 'latest', parentMessageId?: string, limit = 25) {
470
+ let messageSetIndex: number;
471
+ let switchedToMessageSet = false;
472
+ const messageIdToFind = parentMessageId || messageId;
473
+ if (messageId === 'latest') {
474
+ if (this.messages === this.latestMessages) {
475
+ return;
476
+ }
477
+ messageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
478
+ } else {
479
+ messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
480
+ }
481
+ if (messageSetIndex !== -1) {
482
+ this.switchToMessageSet(messageSetIndex);
483
+ switchedToMessageSet = true;
484
+ }
485
+ if (!switchedToMessageSet) {
486
+ await this._channel.query({ messages: { id_around: messageIdToFind, limit } }, 'new');
487
+ }
488
+
489
+ messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
490
+ if (messageSetIndex !== -1) {
491
+ this.switchToMessageSet(messageSetIndex);
492
+ }
493
+ }
494
+
495
+ findMessage(messageId: string, parentMessageId?: string) {
496
+ const messageSetIndex = this.findMessageSetIndex({ id: messageId });
497
+ if (messageSetIndex === -1) {
498
+ return undefined;
499
+ }
500
+ return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
501
+ }
502
+
503
+ private switchToMessageSet(index: number) {
504
+ const currentMessages = this.messageSets.find((s) => s.isCurrent);
505
+ if (!currentMessages) {
506
+ return;
507
+ }
508
+ currentMessages.isCurrent = false;
509
+ this.messageSets[index].isCurrent = true;
510
+ }
511
+
512
+ private areMessageSetsOverlap(messages1: Array<{ id: string }>, messages2: Array<{ id: string }>) {
513
+ return messages1.some((m1) => messages2.find((m2) => m1.id === m2.id));
514
+ }
515
+
516
+ private findMessageSetIndex(message: { id?: string }) {
517
+ return this.messageSets.findIndex((set) => !!set.messages.find((m) => m.id === message.id));
518
+ }
519
+
520
+ private findTargetMessageSet(
521
+ newMessages: MessageResponse<ErmisChatGenerics>[],
522
+ addIfDoesNotExist = true,
523
+ messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
524
+ ) {
525
+ let messagesToAdd: (
526
+ | MessageResponse<ErmisChatGenerics>
527
+ | ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>
528
+ )[] = newMessages;
529
+ let targetMessageSetIndex!: number;
530
+ if (addIfDoesNotExist) {
531
+ const overlappingMessageSetIndices = this.messageSets
532
+ .map((_, i) => i)
533
+ .filter((i) => this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages));
534
+ switch (messageSetToAddToIfDoesNotExist) {
535
+ case 'new':
536
+ if (overlappingMessageSetIndices.length > 0) {
537
+ targetMessageSetIndex = overlappingMessageSetIndices[0];
538
+ } else if (newMessages.some((m) => !m.parent_id)) {
539
+ this.messageSets.push({ messages: [], isCurrent: false, isLatest: false });
540
+ targetMessageSetIndex = this.messageSets.length - 1;
541
+ }
542
+ break;
543
+ case 'current':
544
+ targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
545
+ break;
546
+ case 'latest':
547
+ targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
548
+ break;
549
+ default:
550
+ targetMessageSetIndex = -1;
551
+ }
552
+ // when merging the target set will be the first one from the overlapping message sets
553
+ const mergeTargetMessageSetIndex = overlappingMessageSetIndices.splice(0, 1)[0];
554
+ const mergeSourceMessageSetIndices = [...overlappingMessageSetIndices];
555
+ if (mergeTargetMessageSetIndex !== undefined && mergeTargetMessageSetIndex !== targetMessageSetIndex) {
556
+ mergeSourceMessageSetIndices.push(targetMessageSetIndex);
557
+ }
558
+ // merge message sets
559
+ if (mergeSourceMessageSetIndices.length > 0) {
560
+ const target = this.messageSets[mergeTargetMessageSetIndex];
561
+ const sources = this.messageSets.filter((_, i) => mergeSourceMessageSetIndices.indexOf(i) !== -1);
562
+ sources.forEach((messageSet) => {
563
+ target.isLatest = target.isLatest || messageSet.isLatest;
564
+ target.isCurrent = target.isCurrent || messageSet.isCurrent;
565
+ messagesToAdd = [...messagesToAdd, ...messageSet.messages];
566
+ });
567
+ sources.forEach((s) => this.messageSets.splice(this.messageSets.indexOf(s), 1));
568
+ const overlappingMessageSetIndex = this.messageSets.findIndex((s) =>
569
+ this.areMessageSetsOverlap(s.messages, newMessages),
570
+ );
571
+ targetMessageSetIndex = overlappingMessageSetIndex;
572
+ }
573
+ } else {
574
+ // assumes that all new messages belong to the same set
575
+ targetMessageSetIndex = this.findMessageSetIndex(newMessages[0]);
576
+ }
577
+
578
+ return { targetMessageSetIndex, messagesToAdd };
579
+ }
580
+ }