@ermis-network/ermis-chat-sdk 2.0.0 → 2.0.1

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